| function e(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}export{e as g}; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| :root{--bg-app: #ececec;--bg-toolbar: #e8e8e8;--bg-sidebar: #f5f5f5;--bg-main: #ffffff;--bg-info: #fafafa;--bg-statusbar: #e8e8e8;--bg-modal-overlay: rgba(0, 0, 0, .35);--border: #d4d4d4;--border-light: #e6e6e6;--border-dark: #bbb;--select-bg: #2968c8;--select-bg-hover: #e8f0fe;--select-text: #ffffff;--text: #333333;--text-secondary: #777777;--text-muted: #aaaaaa;--accent: #2968c8;--accent-hover: #1a54b0;--danger: #d93025;--success: #1e8e3e;--warning: #f9a825;--table-header-bg: #f0f0f0;--table-row-alt: #f9f9fc;--table-row-hover: #eef3fb;--table-border: #e2e2e2;--tab-active-bg: #ffffff;--tab-inactive-bg: #e8e8e8;--tab-border: #cccccc;--toolbar-h: 72px;--statusbar-h: 26px;--sidebar-w: 240px;--info-w: 360px;--tab-h: 32px;--font-sans: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", "Segoe UI", sans-serif;--font-mono: "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", monospace;--font-size: 13px;--font-size-sm: 13px;--font-size-xs: 12px}[data-theme=dark]{--bg-app: #1e1e1e;--bg-toolbar: #252526;--bg-sidebar: #1e1e1e;--bg-main: #1e1e1e;--bg-info: #252526;--bg-statusbar: #1e1e1e;--bg-modal-overlay: rgba(0, 0, 0, .6);--border: #3c3c3c;--border-light: #2d2d2d;--border-dark: #4a4a4a;--select-bg: #264f78;--select-bg-hover: #2a2d2e;--select-text: #ffffff;--text: #cccccc;--text-secondary: #969696;--text-muted: #5a5a5a;--accent: #0078d4;--accent-hover: #1a8aed;--danger: #f44747;--success: #4ec9b0;--warning: #cca700;--table-header-bg: #2d2d2d;--table-row-alt: #232323;--table-row-hover: #2a2d2e;--table-border: #3c3c3c;--tab-active-bg: #1e1e1e;--tab-inactive-bg: #2d2d2d;--tab-border: #3c3c3c;color-scheme:dark}[data-theme=dark] input,[data-theme=dark] select,[data-theme=dark] textarea{background:#2d2d2d;color:#ccc;border-color:#3c3c3c}[data-theme=dark] .history-panel,[data-theme=dark] .combo-box-dropdown{background:#252526}[data-theme=dark] .cli-container{background:#1b1b1b}[data-theme=dark] .cli-output{color:#ccc}[data-theme=dark] .cli-welcome{color:#5a5a5a;border-bottom-color:#3c3c3c}[data-theme=dark] .cli-result{color:#969696}[data-theme=dark] .import-error-list{background:#3a2020;border-color:#5a3030}[data-theme=dark] .toolbar-btn:hover{background:#ffffff14}[data-theme=dark] .toolbar-btn:active{background:#ffffff1f}[data-theme=dark] .toolbar-btn.active{background:#264f78;color:#6cb6ff}*,*:before,*:after{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);font-size:var(--font-size);color:var(--text);background:var(--bg-app);overflow:hidden}button,input,textarea,select{font:inherit}button{cursor:pointer;border:none;background:none}ul,ol{list-style:none;margin:0;padding:0}#root{height:100vh}.app-shell{display:flex;flex-direction:column;height:100vh;overflow:hidden}.toolbar{height:var(--toolbar-h);background:var(--bg-toolbar);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 12px;gap:2px;flex-shrink:0;-webkit-user-select:none;user-select:none}.toolbar-btn{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;padding:6px 12px;border-radius:6px;color:var(--text);font-size:var(--font-size-xs);min-width:56px;transition:background .12s}.toolbar-btn:hover{background:#0000000f}.toolbar-btn:active{background:#0000001a}.toolbar-btn.active{background:#e8f0fe;color:#1a73e8}.toolbar-btn .tb-icon{font-size:24px;line-height:1;display:flex;align-items:center;justify-content:center;width:32px;height:32px}.toolbar-btn .tb-label{white-space:nowrap}.toolbar-sep{width:1px;height:44px;background:var(--border);margin:0 6px;flex-shrink:0}.toolbar-spacer{flex:1}.workspace{flex:1;display:flex;overflow:hidden;min-height:0}.sidebar{width:var(--sidebar-w);min-width:140px;background:var(--bg-sidebar);border-right:none;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}.panel-resize-handle.vertical{width:5px;cursor:col-resize;background:var(--border);flex-shrink:0;position:relative;transition:background .15s;z-index:5;align-self:stretch}.panel-resize-handle.vertical:before{content:"";position:absolute;inset:0 -3px;z-index:6}.panel-resize-handle.vertical:hover,.panel-resize-handle.vertical:active{background:var(--accent)}.panel-resize-handle.vertical:after{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);height:30px;width:2px;border-left:1px solid rgba(0,0,0,.15);border-right:1px solid rgba(0,0,0,.15)}.sidebar-header{padding:8px 12px 6px;font-size:var(--font-size-sm);font-weight:600;color:var(--text-secondary);border-bottom:1px solid var(--border-light);display:flex;align-items:center;gap:6px;flex-shrink:0}.sidebar-search{margin:6px 8px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-sm);background:#fff;outline:none;flex-shrink:0}.sidebar-search:focus{border-color:var(--accent)}.sidebar-search::placeholder{color:var(--text-muted)}.tree-container{flex:1;overflow-y:auto;overflow-x:hidden;padding:2px 0}.tree-node{display:flex;align-items:center;padding:2px 8px 2px 0;cursor:pointer;font-size:var(--font-size-sm);white-space:nowrap;-webkit-user-select:none;user-select:none;border:1px solid transparent;min-height:24px}.tree-node:hover{background:var(--select-bg-hover)}.tree-node.selected{background:var(--select-bg);color:var(--select-text)}.tree-node.selected:hover{background:var(--accent-hover)}.tree-arrow{width:16px;height:16px;display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--text-secondary);flex-shrink:0;transition:transform .12s ease}.tree-arrow.expanded{transform:rotate(90deg)}.tree-node.selected .tree-arrow{color:var(--select-text)}.conn-color-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.tree-icon{width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:4px;font-size:14px}.tree-label{overflow:hidden;text-overflow:ellipsis}.main-area{flex:1;display:flex;flex-direction:column;min-width:0;background:var(--bg-main);overflow:hidden}.tab-bar{display:flex;align-items:flex-end;background:var(--tab-inactive-bg);border-bottom:1px solid var(--border);flex-shrink:0;min-height:var(--tab-h);overflow-x:auto;overflow-y:hidden;-webkit-user-select:none;user-select:none}.tab-bar::-webkit-scrollbar{height:0}.tab-item{display:flex;align-items:center;gap:6px;padding:6px 12px;font-size:var(--font-size-sm);border:1px solid var(--tab-border);border-bottom:none;border-radius:4px 4px 0 0;background:var(--tab-inactive-bg);color:var(--text-secondary);margin-left:-1px;cursor:pointer;white-space:nowrap;max-width:200px;position:relative;transition:background .1s}.tab-item:first-child{margin-left:4px}.tab-item:hover{background:#f0f0f0}.tab-item.active{background:var(--tab-active-bg);color:var(--text);border-bottom:1px solid var(--tab-active-bg);z-index:1;font-weight:500}.tab-item .tab-icon{font-size:14px}.tab-item .tab-title{overflow:hidden;text-overflow:ellipsis}.tab-close{width:16px;height:16px;display:flex;align-items:center;justify-content:center;border-radius:3px;font-size:14px;color:var(--text-muted);flex-shrink:0;line-height:1}.tab-close:hover{background:#0000001a;color:var(--text)}.tab-item.active .tab-close{color:var(--text-secondary)}.tab-content{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0}.empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;color:var(--text-muted);font-size:14px}.empty-state .empty-icon{font-size:48px;opacity:.4}.empty-state .empty-title{font-size:16px;color:var(--text-secondary);font-weight:500}.objects-toolbar{display:flex;align-items:center;padding:6px 8px;gap:6px;border-bottom:1px solid var(--border-light);flex-shrink:0}.objects-toolbar button{padding:3px 6px;border-radius:3px;font-size:var(--font-size-sm);color:var(--text-secondary)}.objects-toolbar button:hover{background:#0000000f}.objects-search{padding:3px 8px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-sm);width:180px;background:#fff;outline:none;margin-left:auto}.objects-search:focus{border-color:var(--accent)}.objects-view-toggle{display:flex;gap:2px}.view-btn{padding:4px 6px!important;font-size:14px!important}.view-btn.active{background:#00000014!important}.objects-grid{flex:1;overflow:auto}.objects-table{width:100%;border-collapse:collapse}.objects-table th{position:sticky;top:0;background:var(--table-header-bg);border-bottom:1px solid var(--table-border);padding:5px 12px;text-align:left;font-size:var(--font-size-sm);font-weight:500;color:var(--text-secondary);white-space:nowrap;z-index:1}.objects-table td{padding:4px 12px;border-bottom:1px solid var(--border-light);font-size:var(--font-size-sm);white-space:nowrap}.objects-table tr:hover td{background:var(--table-row-hover)}.objects-table .obj-icon{margin-right:6px;font-size:14px}.data-toolbar{display:flex;align-items:center;padding:4px 8px;gap:4px;border-bottom:1px solid var(--border-light);flex-shrink:0;background:var(--bg-info)}.data-toolbar button{padding:3px 8px;border-radius:3px;font-size:var(--font-size-sm);color:var(--text-secondary)}.data-toolbar button:hover{background:#0000000f}.filter-label{font-size:var(--font-size-xs);color:var(--text-secondary);white-space:nowrap}.filter-input{flex:1;max-width:300px;min-width:120px;padding:2px 8px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-xs);font-family:var(--font-mono);outline:none;transition:border-color .15s}.filter-input:focus{border-color:var(--accent)}.filter-clear{background:none;border:none;color:var(--danger);cursor:pointer;font-size:12px;padding:2px 4px;border-radius:3px}.filter-clear:hover{background:#fee}.sort-indicator{font-size:11px;color:var(--accent);font-weight:700}.col-header{position:relative;cursor:default}.col-header-label{flex:1}.col-sort-trigger{opacity:0;font-size:10px;color:var(--fg-dim);cursor:pointer;padding:0 2px;transition:opacity .15s;-webkit-user-select:none;user-select:none}.col-header:hover .col-sort-trigger{opacity:1}.col-sort-menu{position:absolute;top:100%;left:0;z-index:200;background:#fff;border:1px solid var(--border);border-radius:6px;box-shadow:0 4px 16px #00000038;min-width:140px;padding:4px 0;font-size:13px;font-weight:400;white-space:nowrap}.col-sort-item{padding:6px 12px;cursor:pointer;display:flex;align-items:center;gap:2px}.col-sort-item:hover{background:var(--hover)}.col-sort-sep{height:1px;background:var(--border);margin:4px 0}.filter-tag{display:inline-flex;align-items:center;gap:4px;background:#e8f0fe;color:#1a73e8;font-size:12px;padding:2px 8px;border-radius:12px;margin-right:4px;white-space:nowrap}.filter-tag strong{font-weight:600}.filter-tag-close{background:none;border:none;color:#1a73e8;cursor:pointer;font-size:11px;padding:0 2px;line-height:1}.filter-tag-close:hover{color:#c62828}.filter-clear-all{background:none;border:1px solid var(--border);border-radius:4px;color:var(--fg-dim);font-size:11px;cursor:pointer;padding:2px 8px;margin-left:4px}.filter-clear-all:hover{background:var(--hover)}.toolbar-icon-btn{background:none;border:1px solid transparent;border-radius:4px;font-size:16px;cursor:pointer;padding:2px 6px}.toolbar-icon-btn:hover{background:var(--hover)}.toolbar-icon-btn.active{background:#e8f0fe;border-color:#4285f4}.navi-filter-panel{border-bottom:1px solid #d8d8d8;background:#fafafa}.navi-filter-conditions{padding:10px 14px;min-height:40px}.navi-filter-row{display:flex;align-items:center;gap:6px;padding:3px 0;font-size:13px}.navi-filter-check{width:16px;height:16px;margin:0;cursor:pointer;accent-color:#4285f4}.navi-filter-col-select{color:#06c;font-weight:600;font-size:13px;border:none;background:transparent;cursor:pointer;outline:none;padding:2px 4px;appearance:none;-webkit-appearance:none}.navi-filter-col-select:hover{text-decoration:underline}.navi-filter-op{color:#333;padding:1px 2px;border:1px solid #d0d0d0;border-radius:3px;background:#fff;font-size:12px;cursor:pointer;min-width:50px;outline:none}.navi-filter-val{color:#333;cursor:pointer;padding:2px 8px;background:#fff;border:1px solid #d0d0d0;border-radius:3px;min-width:20px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.navi-filter-val:hover{border-color:#999}.navi-filter-connector{color:#999;font-size:12px;padding:0 4px}.navi-filter-btn-icon{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;font-size:14px;font-weight:700;color:#fff;background:#4285f4;border-radius:3px;cursor:pointer;margin-left:2px}.navi-filter-btn-icon:hover{background:#3367d6}.navi-filter-btn-group{background:#7baaf7;font-size:11px}.navi-filter-btn-group:hover{background:#5a94e6}.navi-filter-remove{background:none;border:none;color:#ccc;cursor:pointer;font-size:12px;padding:0 4px;margin-left:auto}.navi-filter-remove:hover{color:#c62828}.navi-filter-group{margin:2px 0}.navi-filter-group-paren{font-size:15px;font-weight:600;color:#555;padding:0 2px}.navi-filter-group-children{padding-left:24px}.navi-filter-toolbar{display:flex;align-items:center;gap:2px;padding:4px 14px;background:#f0f0f0;border-top:1px solid #e0e0e0}.navi-filter-tool{width:26px;height:24px;display:flex;align-items:center;justify-content:center;font-size:14px;color:#555;cursor:pointer;border-radius:4px}.navi-filter-tool:hover{background:#d6d6d6}.navi-filter-apply{font-size:13px;color:#333;cursor:pointer;padding:2px 10px;border-radius:4px;font-weight:500}.navi-filter-apply:hover{background:#d6d6d6}.navi-filter-status{font-size:12px;color:#999;margin-left:8px}.navi-picker-overlay{position:absolute;inset:0;z-index:300}.navi-picker{position:absolute;top:60px;right:40px;width:380px;max-height:440px;background:#f6f6f6;border-radius:10px;box-shadow:0 8px 32px #00000040;display:flex;flex-direction:column;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.navi-picker-input{margin:12px 14px 0;padding:7px 8px;border:2px solid #4a90d9;border-radius:4px;font-size:13px;outline:none;background:#fff}.navi-picker-input:focus{border-color:#2563eb}.navi-picker-section{padding:10px 14px 4px;font-size:13px;color:#666;font-weight:500}.navi-picker-list{flex:1;overflow-y:auto;min-height:60px;max-height:220px;margin:4px 10px;background:#fff;border:1px solid #d4d4d4;border-radius:6px}.navi-picker-loading{text-align:center;padding:20px;color:#999;font-size:13px}.navi-picker-item{display:flex;align-items:center;gap:8px;padding:5px 10px;cursor:pointer;font-size:13px;border-bottom:1px solid #f0f0f0;color:#333}.navi-picker-item:last-child{border-bottom:none}.navi-picker-item:hover{background:#f0f5ff}.navi-picker-item input[type=checkbox]{width:15px;height:15px;cursor:pointer;flex-shrink:0;margin:0}.navi-picker-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.navi-picker-search-row{display:flex;align-items:center;margin:6px 14px;border:1px solid #d4d4d4;border-radius:6px;background:#fff;overflow:hidden}.navi-picker-search-icon{padding:5px 6px 5px 10px;font-size:14px;color:#999;flex-shrink:0}.navi-picker-search{flex:1;border:none;outline:none;padding:6px 8px 6px 2px;font-size:13px;background:transparent}.navi-picker-actions{display:flex;justify-content:flex-end;gap:8px;padding:8px 14px 12px}.navi-picker-btn{padding:5px 22px;border:1px solid #c0c0c0;border-radius:6px;background:#fff;font-size:13px;cursor:pointer;color:#333}.navi-picker-btn:hover{background:#eaeaea}.navi-picker-btn-ok{background:#4285f4;color:#fff;border-color:#4285f4;font-weight:500}.navi-picker-btn-ok:hover{background:#3367d6}.data-grid-wrap{flex:1;overflow:auto;min-height:0}.data-grid{width:auto;border-collapse:collapse;table-layout:auto}.query-result-grid{width:max-content;table-layout:fixed}.data-grid th{top:0;background:var(--table-header-bg);border-bottom:2px solid var(--table-border);border-right:1px solid var(--table-border);padding:4px 10px;text-align:left;font-size:var(--font-size-sm);font-weight:600;white-space:nowrap;z-index:1;min-width:60px;position:sticky;overflow:visible}.col-resize-handle{position:absolute;right:-2px;top:0;bottom:0;width:5px;cursor:col-resize;z-index:3;-webkit-user-select:none;user-select:none}.col-resize-handle:hover,.col-resize-handle:active{background:var(--accent)}.data-grid td{padding:4px 10px;border-bottom:1px solid var(--border-light);border-right:1px solid var(--border-light);font-size:var(--font-size-xs);white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis}.data-grid tr:nth-child(2n) td{background:var(--table-row-alt)}.data-grid tr:hover td{background:var(--table-row-hover)}.data-grid tr.row-selected td{background:#cce5ff!important}.data-grid tr.row-selected:hover td{background:#b8daff!important}.data-grid tr.row-selected .row-num{background:#b8daff!important;color:#004085}.data-grid td.cell-selected{outline:2px solid #4285f4;outline-offset:-2px}.data-grid tr{cursor:default}.data-grid .row-num{color:var(--text-muted);text-align:right;background:var(--table-header-bg);border-right:2px solid var(--table-border);font-family:var(--font-mono);font-size:var(--font-size-xs);min-width:40px;padding:3px 6px;position:sticky;left:0;z-index:0}.data-grid th.row-num-header{min-width:40px;z-index:2;position:sticky;left:0}.cell-null{color:var(--text-muted);font-style:italic}.data-footer{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;border-top:1px solid var(--border-light);background:var(--bg-info);font-size:var(--font-size-xs);color:var(--text-secondary);flex-shrink:0;gap:8px;flex-wrap:wrap}.data-footer-right{display:flex;align-items:center;gap:12px}.data-footer-sql{font-family:var(--font-mono);color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50%}.cell-edit-input{width:100%;min-width:60px;padding:1px 4px;border:2px solid var(--accent);border-radius:2px;font-family:var(--font-mono);font-size:var(--font-size-xs);outline:none;background:#fff;box-sizing:border-box}.cell-modified{background:#fff3cd!important}.data-grid tr.row-new td{background:#d4edda!important}.data-grid tr.row-new .row-num{background:#c3e6cb!important;color:#155724}.data-grid tr.row-deleted td{background:#f8d7da!important;text-decoration:line-through;color:#999}.data-grid tr.row-deleted .row-num{background:#f5c6cb!important;color:#721c24}.data-action-bar{display:flex;align-items:center;gap:2px;flex-shrink:0}.action-btn{width:26px;height:22px;border:1px solid var(--border);border-radius:3px;background:#fff;cursor:pointer;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);transition:background .1s,color .1s}.action-btn:hover:not(:disabled){background:var(--table-row-hover);color:var(--text)}.action-btn:disabled{opacity:.35;cursor:default}.action-btn.apply{color:#28a745;font-weight:700}.action-btn.apply:hover:not(:disabled){background:#d4edda}.action-btn.cancel{color:#dc3545;font-weight:700}.action-btn.cancel:hover:not(:disabled){background:#f8d7da}.action-status{font-size:var(--font-size-xs);color:var(--text-secondary);margin-left:8px}.pagination{display:flex;align-items:center;gap:2px}.page-btn{padding:2px 8px!important;border:1px solid var(--border)!important;border-radius:3px!important;font-size:11px!important;background:#fff!important;cursor:pointer!important;line-height:1.4!important;min-width:24px!important;text-align:center!important}.page-btn:hover{background:var(--select-bg-hover)!important}.page-btn:disabled{opacity:.35;cursor:default!important;background:#f5f5f5!important}.page-info{padding:0 6px;font-size:11px;color:var(--text-secondary);white-space:nowrap}.page-input{width:38px;padding:1px 4px;margin:0 4px;border:1px solid var(--border);border-radius:3px;font-size:11px;font-weight:600;color:var(--accent);text-align:center;background:#fff;outline:none;line-height:1.4}.page-input:focus{border-color:var(--accent);box-shadow:0 0 0 2px #2968c826}.page-count{font-size:11px;color:var(--text-secondary);white-space:nowrap;padding-left:4px;border-left:1px solid var(--border-light)}.query-pane{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}.query-toolbar{display:flex;align-items:center;padding:2px 8px;gap:2px;border-bottom:1px solid var(--border-light);flex-shrink:0;background:var(--bg-info);height:32px}.query-toolbar select{padding:2px 6px;border:1px solid var(--border);border-radius:3px;font-size:var(--font-size-sm);background:var(--bg-main);color:var(--text)}.query-snippet-select{min-width:88px;max-width:160px;height:24px}.query-db-selector{display:flex;align-items:center;gap:4px}.query-db-selector select{padding:2px 20px 2px 6px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:var(--bg-main);min-width:140px;cursor:pointer;appearance:auto;color:var(--text);height:24px}.query-db-selector select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px #2968c826}.tb-icon-btn{display:flex;align-items:center;justify-content:center;width:28px;height:24px;border:none;background:transparent;color:var(--text-muted, #666);border-radius:4px;cursor:pointer;padding:0;transition:all .12s ease}.tb-icon-btn:hover{background:#0000000f;color:var(--text)}.tb-icon-btn:active{background:#0000001a}.tb-icon-btn:disabled{opacity:.3;cursor:default}.tb-icon-btn:disabled:hover{background:transparent}.tb-icon-btn.tb-run{color:#2ea043}.tb-icon-btn.tb-run:hover{background:#2ea0431a;color:#238636}.toolbar-sep{width:1px;height:16px;background:var(--border);margin:0 4px;flex-shrink:0}.run-btn{padding:3px 12px!important;background:var(--accent)!important;color:#fff!important;border-radius:4px!important;font-weight:500!important;font-size:var(--font-size-sm)!important}.run-btn:hover{background:var(--accent-hover)!important}.run-btn:disabled{opacity:.5}.query-split-container{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}.query-editor-area{min-height:60px;height:200px;flex-shrink:0;position:relative;overflow:hidden}.query-editor-area .cm-editor,.query-editor-area .cm-scroller,.query-editor-area .cm-content,.query-editor-area .cm-line{user-select:text!important;-webkit-user-select:text!important}.query-split-handle{height:4px;background:var(--border-light);cursor:row-resize;flex-shrink:0;position:relative;transition:background .15s}.query-split-handle:hover,.query-split-handle:active{background:var(--accent)}.query-split-handle:after{content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:30px;height:2px;border-top:1px solid rgba(0,0,0,.15);border-bottom:1px solid rgba(0,0,0,.15)}.query-results{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden;contain:strict;isolation:isolate}.result-view-tabs{display:flex;align-items:stretch;background:var(--bg-info);border-bottom:1px solid var(--border-light);flex-shrink:0;gap:0}.result-view-actions{display:flex;align-items:center;gap:6px;padding:0 8px}.result-action-btn{min-width:38px;height:26px;padding:0 10px;border:1px solid var(--border);border-radius:6px;background:#fff;cursor:pointer;font-size:12px;line-height:1;font-weight:600;display:inline-flex;align-items:center;justify-content:center;color:var(--text-secondary);transition:background .12s,color .12s,border-color .12s,box-shadow .12s;white-space:nowrap}.result-action-btn:hover{background:var(--table-row-hover);color:var(--text);border-color:var(--accent-soft)}.result-action-btn:focus-visible{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px #2968c829}.result-action-btn-icon{min-width:30px;width:30px;padding:0;font-size:13px}.result-view-tab{padding:6px 16px;font-size:var(--font-size-xs);color:var(--text-secondary);border:1px solid transparent;border-bottom:none;border-radius:4px 4px 0 0;cursor:pointer;background:none;margin-bottom:-1px;transition:all .12s}.result-view-tab:hover{color:var(--text);background:#00000008}.result-view-tab.active{color:var(--accent);background:#fff;border-color:var(--border-light);font-weight:500}.result-grid-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0;contain:layout paint}.virtual-grid-scroll{contain:layout paint;isolation:isolate;will-change:scroll-position}.query-messages{padding:12px 16px;font-family:var(--font-mono);font-size:var(--font-size-xs);line-height:1.7;color:var(--text);overflow:auto}.query-message-item{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--border-light)}.query-message-item:last-child{border-bottom:none;margin-bottom:0}.qm-sql{color:var(--text);word-break:break-all}.qm-info{color:#666}.qm-time{color:#999}.info-panel{width:clamp(240px,20vw,500px);background:var(--bg-info);border-left:none;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}.info-header{padding:16px 16px 12px;border-bottom:1px solid var(--border-light);display:flex;align-items:center;gap:12px;flex-shrink:0}.info-icon{width:48px;height:48px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:28px;flex-shrink:0}.info-icon.conn-icon{background:linear-gradient(135deg,#e8d5b7,#c9a96e)}.info-icon.db-icon{background:linear-gradient(135deg,#b8e6c8,#5cb87a)}.info-icon.table-icon{background:linear-gradient(135deg,#b8d4f0,#5a9fd4)}.info-title{font-size:16px;font-weight:600;line-height:1.2}.info-subtitle{font-size:var(--font-size-xs);color:var(--text-muted);margin-top:2px}.info-body{flex:1;overflow-y:auto;padding:12px 16px}.info-row{display:flex;flex-direction:column;gap:2px;margin-bottom:10px}.info-label{font-size:var(--font-size-xs);color:var(--text-muted);font-weight:500}.info-value{font-size:var(--font-size-sm);color:var(--text);word-break:break-all}.info-tab-bar{display:flex;padding:0 8px;border-bottom:1px solid var(--border-light);flex-shrink:0;background:var(--bg-info);gap:0}.info-tab{padding:6px 12px;font-size:var(--font-size-xs);color:var(--text-secondary);border:none;border-bottom:2px solid transparent;background:none;cursor:pointer;border-radius:0;font-weight:500;transition:color .12s,border-color .12s}.info-tab:hover{color:var(--text)}.info-tab.active{color:var(--accent);border-bottom-color:var(--accent)}.ddl-area{margin-top:0;padding:12px;background:#f8f8f8;border:none;border-radius:0;font-family:var(--font-mono);font-size:var(--font-size-xs);line-height:1.6;white-space:pre;overflow:auto;color:var(--text);-webkit-user-select:text;user-select:text}.sql-kw{color:#1a0dab}.sql-type{color:#0b7285}.sql-str{color:#e65100}.sql-num{color:#6a1b9a}.sql-comment{color:#6a9955;font-style:italic}.sql-punct{color:#777}.ddl-body{padding:0!important}.ddl-body .ddl-area{flex:1;max-height:none;height:100%}[data-theme=dark] .ddl-area{background:#1e1e1e}[data-theme=dark] .sql-kw{color:#569cd6}[data-theme=dark] .sql-type{color:#4ec9b0}[data-theme=dark] .sql-str{color:#ce9178}[data-theme=dark] .sql-num{color:#b5cea8}[data-theme=dark] .sql-comment{color:#6a9955}[data-theme=dark] .sql-punct{color:gray}.columns-body{padding:0!important;overflow:auto}.info-inline-actions{display:flex;justify-content:flex-end;gap:8px;padding:8px;border-bottom:1px solid var(--border-light);background:var(--bg-info)}.info-columns-table{width:100%;border-collapse:collapse;font-size:var(--font-size-xs)}.info-columns-table th{position:sticky;top:0;background:var(--table-header-bg);border-bottom:1px solid var(--table-border);padding:4px 8px;text-align:left;font-weight:600;color:var(--text-secondary);white-space:nowrap;z-index:1}.info-columns-table td{padding:3px 8px;border-bottom:1px solid var(--border-light);white-space:nowrap}.info-columns-table tr:hover td{background:var(--table-row-hover)}.info-columns-table .col-name{font-weight:500;font-family:var(--font-mono);color:var(--text)}.info-columns-table .col-type{color:var(--accent);font-family:var(--font-mono)}.info-columns-table .col-key{color:var(--warning);font-weight:600}.status-bar{height:var(--statusbar-h);background:var(--bg-statusbar);border-top:1px solid var(--border);display:flex;align-items:center;padding:0 12px;gap:16px;font-size:var(--font-size-xs);color:var(--text-secondary);flex-shrink:0;-webkit-user-select:none;user-select:none}.status-dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:4px}.status-dot.online{background:var(--success)}.status-dot.offline{background:var(--text-muted)}.modal-overlay{position:fixed;inset:0;background:var(--bg-modal-overlay);display:flex;align-items:center;justify-content:center;z-index:1000;animation:fade-in .15s ease-out}.modal-card{width:560px;max-height:80vh;background:#fff;border-radius:8px;box-shadow:0 12px 40px #00000040;display:flex;flex-direction:column;animation:slide-up .2s ease-out}.modal-title-bar{padding:14px 20px 12px;border-bottom:1px solid var(--border-light);display:flex;align-items:center;justify-content:space-between}.modal-title-bar h3{margin:0;font-size:14px;font-weight:600}.modal-close{width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;font-size:18px;color:var(--text-secondary)}.modal-close:hover{background:#0000000f}.modal-tabs{display:flex;padding:0 20px;border-bottom:1px solid var(--border-light)}.modal-tab{padding:8px 14px;font-size:var(--font-size-sm);color:var(--text-secondary);border-bottom:2px solid transparent}.modal-tab:hover{color:var(--text)}.modal-tab.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:500}.modal-body{padding:20px;flex:1;overflow-y:auto}.modal-row{display:grid;grid-template-columns:80px 1fr;align-items:center;gap:8px;margin-bottom:12px}.modal-row label{font-size:var(--font-size-sm);color:var(--text-secondary);text-align:right}.modal-row input{padding:6px 10px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-sm);outline:none;background:#fff}.modal-row input:focus{border-color:var(--accent)}.modal-row-split{display:grid;grid-template-columns:80px 1fr 60px 1fr;align-items:center;gap:8px;margin-bottom:12px}.modal-row-split label{font-size:var(--font-size-sm);color:var(--text-secondary);text-align:right}.modal-row-split input{padding:6px 10px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-sm);outline:none;background:#fff}.modal-row-split input:focus{border-color:var(--accent)}.modal-diagram{display:flex;align-items:center;justify-content:center;gap:20px;padding:12px 0 20px}.modal-diagram-node{display:flex;flex-direction:column;align-items:center;gap:4px;font-size:var(--font-size-sm);color:var(--text-secondary)}.modal-diagram-icon{font-size:32px}.modal-diagram-line{width:40px;height:2px;background:var(--border)}.modal-checkbox{display:flex;align-items:center;gap:8px;margin-bottom:12px;padding-left:88px}.modal-checkbox input{width:16px;height:16px}.modal-checkbox span{font-size:var(--font-size-sm);color:var(--text-secondary)}.modal-footer{padding:12px 20px;border-top:1px solid var(--border-light);display:flex;align-items:center;justify-content:space-between;gap:8px}.modal-footer-left,.modal-footer-right{display:flex;gap:8px}.btn{padding:6px 16px;border-radius:4px;font-size:var(--font-size-sm);font-weight:500;border:1px solid var(--border);background:#fff;color:var(--text);transition:background .1s}.btn:hover{background:#f0f0f0}.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}.btn-primary:hover{background:var(--accent-hover)}.btn-danger{color:var(--danger);border-color:#d930254d}.btn-danger:hover{background:#d930250f}.btn-sm{padding:3px 10px;font-size:var(--font-size-xs)}.btn:disabled{opacity:.5;cursor:default}.btn-link{color:var(--accent);font-size:var(--font-size-sm);padding:4px 8px}.btn-link:hover{text-decoration:underline}.profiles-section{margin-top:16px;border-top:1px solid var(--border-light);padding-top:12px}.profiles-section h4{margin:0 0 8px;font-size:var(--font-size-sm);font-weight:600;color:var(--text-secondary)}.profile-item{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;border-radius:4px;cursor:pointer;font-size:var(--font-size-sm)}.profile-item:hover{background:var(--select-bg-hover)}.profile-item .profile-name{font-weight:500}.profile-item .profile-host{color:var(--text-muted);font-size:var(--font-size-xs)}.profile-actions{display:flex;gap:4px}@keyframes slide-up{0%{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}.resize-handle{width:4px;cursor:col-resize;background:transparent;transition:background .2s;flex-shrink:0}.resize-handle:hover{background:var(--accent)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:#ccc;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#aaa}.context-menu-overlay{position:fixed;inset:0;z-index:800}.context-menu{position:fixed;z-index:801;min-width:200px;max-height:calc(100vh - 16px);overflow-y:auto;background:#fffffff5;-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);border:1px solid rgba(0,0,0,.12);border-radius:8px;box-shadow:0 8px 32px #0000002e,0 2px 8px #00000014;padding:4px 0;animation:ctx-pop .12s ease-out;font-size:var(--font-size-sm);-webkit-user-select:none;user-select:none}.ctx-item{display:flex;align-items:center;gap:8px;padding:6px 16px 6px 12px;cursor:pointer;color:var(--text);border-radius:4px;margin:0 4px;white-space:nowrap}.ctx-item:hover{background:var(--select-bg);color:#fff}.ctx-item.danger{color:var(--danger)}.ctx-item.danger:hover{background:var(--danger);color:#fff}.ctx-item.disabled{color:var(--text-muted);cursor:default}.ctx-item.disabled:hover{background:none;color:var(--text-muted)}.ctx-item .ctx-icon{width:18px;text-align:center;font-size:14px;flex-shrink:0}.ctx-item .ctx-label{flex:1}.ctx-item .ctx-shortcut{color:var(--text-muted);font-size:var(--font-size-xs);margin-left:16px}.ctx-item:hover .ctx-shortcut{color:#ffffffb3}.ctx-item.has-submenu{position:relative}.ctx-submenu{display:none;position:absolute;left:100%;top:-4px;min-width:200px;background:var(--bg-info);border:1px solid var(--border);border-radius:8px;padding:4px 0;box-shadow:0 6px 24px #00000026;z-index:1100}.ctx-item.has-submenu:hover>.ctx-submenu{display:block}.ctx-sep{height:1px;background:#00000014;margin:4px 8px}@keyframes ctx-pop{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}[data-theme=dark] .context-menu{background:#2d2d2df7;border-color:#4a4a4a;box-shadow:0 8px 32px #00000080}[data-theme=dark] .ctx-sep{background:#ffffff14}[data-theme=dark] .ctx-submenu{background:#2d2d2df7;border-color:#4a4a4a;box-shadow:0 8px 32px #00000080}[data-theme=dark] .context-menu-overlay{background:transparent}[data-theme=dark] .cm-editor{background:#1e1e1e!important}[data-theme=dark] .cm-editor .cm-content{caret-color:#aeafad}[data-theme=dark] .cm-editor .cm-cursor{border-left-color:#aeafad!important}[data-theme=dark] .cm-editor .cm-activeLine{background:#2a2a2a!important}[data-theme=dark] .cm-editor .cm-gutters{background:#1e1e1e!important;border-right-color:#3c3c3c!important;color:#5a5a5a!important}[data-theme=dark] .cm-editor .cm-activeLineGutter{background:#2a2a2a!important;color:#ccc!important}[data-theme=dark] .cm-editor .cm-selectionBackground{background:#264f78!important}[data-theme=dark] .cm-editor .cm-matchingBracket{background:#3a5a3a!important;outline-color:#5a8a5a!important}[data-theme=dark] .cm-editor .cm-panels{background:#252526!important;border-bottom-color:#3c3c3c!important}[data-theme=dark] .cm-editor .cm-tooltip{background:#252526!important;border-color:#4a4a4a!important}[data-theme=dark] .cm-editor .cm-tooltip-autocomplete ul>li[aria-selected]{background:#264f78!important}[data-theme=dark] .cm-editor .cm-completionDetail{color:#6a6a6a!important}[data-theme=dark] .result-view-tab.active{background:var(--bg-main);color:#6cb6ff;border-color:var(--border-light)}[data-theme=dark] .result-view-tab:hover{background:#ffffff0d}[data-theme=dark] .qm-info{color:#8b949e}[data-theme=dark] .qm-time{color:#6e7681}[data-theme=dark] .query-split-handle{background:#30363d}[data-theme=dark] .query-split-handle:after{border-top-color:#ffffff26;border-bottom-color:#ffffff26}[data-theme=dark] .data-grid th{background:#21262d;color:#c9d1d9;border-color:#30363d}[data-theme=dark] .data-grid td{border-color:#21262d}[data-theme=dark] .data-grid tr:nth-child(2n) td{background:#ffffff05}[data-theme=dark] .data-grid tr:hover td{background:#ffffff0f}[data-theme=dark] .data-grid tr.row-selected td{background:#1c3a5c!important}[data-theme=dark] .data-grid tr.row-selected:hover td{background:#214a6e!important}[data-theme=dark] .data-grid tr.row-selected .row-num{background:#214a6e!important;color:#6cb6ff}[data-theme=dark] .data-grid .row-num{background:#161b22;color:#484f58;border-color:#21262d}[data-theme=dark] .data-grid td.cell-selected{outline-color:#58a6ff}[data-theme=dark] .col-sort-popup{background:#21262d;border-color:#30363d}[data-theme=dark] .col-sort-item:hover{background:#ffffff14}[data-theme=dark] .page-btn{color:#8b949e;border-color:#30363d}[data-theme=dark] .page-btn:hover:not(:disabled){background:#ffffff14}[data-theme=dark] .page-input{background:#0d1117;color:#c9d1d9;border-color:#30363d}[data-theme=dark] .page-count{color:#6e7681}[data-theme=dark] .summary-kv{border-color:#21262d}[data-theme=dark] .summary-kv .sk-label{color:#8b949e}[data-theme=dark] .summary-kv .sk-value{color:#c9d1d9}[data-theme=dark] .query-toolbar{background:#161b22;border-bottom-color:#30363d}[data-theme=dark] .tb-icon-btn{color:#8b949e}[data-theme=dark] .tb-icon-btn:hover{background:#ffffff14;color:#c9d1d9}[data-theme=dark] .tb-icon-btn:active{background:#ffffff1f}[data-theme=dark] .tb-icon-btn.tb-run{color:#3fb950}[data-theme=dark] .tb-icon-btn.tb-run:hover{background:#3fb95026;color:#3fb950}[data-theme=dark] .toolbar-sep{background:#30363d}[data-theme=dark] .run-btn{background:#238636!important;color:#fff!important}[data-theme=dark] .run-btn:hover{background:#2ea043!important}[data-theme=dark] .query-toolbar select,[data-theme=dark] .query-db-selector select{background:#21262d;color:#c9d1d9;border-color:#30363d}[data-theme=dark] .query-db-selector select:focus{border-color:#58a6ff;box-shadow:0 0 0 2px #58a6ff33}[data-theme=dark] .result-view-tab.active{background:#0d1117;border-color:#30363d}[data-theme=dark] .history-panel{background:#161b22;border-color:#30363d}[data-theme=dark] .history-item{border-bottom-color:#21262d}[data-theme=dark] .history-item:hover{background:#ffffff0a}[data-theme=dark] .history-panel-header{border-bottom-color:#30363d}[data-theme=dark] .empty-state{color:#484f58}[data-theme=dark] .tab-bar{background:#161b22;border-bottom-color:#30363d}[data-theme=dark] .tab-item{color:#8b949e;border-color:transparent}[data-theme=dark] .tab-item:hover{background:#ffffff0a}[data-theme=dark] .tab-item.active{background:var(--bg-main);color:#c9d1d9;border-color:#30363d;border-bottom-color:var(--bg-main)}[data-theme=dark] .tab-close:hover{background:#ffffff1a}[data-theme=dark] .action-bar{background:#161b22;border-bottom-color:#30363d}[data-theme=dark] .action-btn{color:#8b949e}[data-theme=dark] .action-btn:hover{background:#ffffff14;color:#c9d1d9}[data-theme=dark] .pagination-bar{background:#161b22;border-top-color:#30363d}.notice-toast{position:fixed;top:calc(var(--toolbar-h) + 8px);right:12px;padding:8px 16px;border-radius:6px;font-size:var(--font-size-sm);z-index:900;animation:slide-in-right .2s ease-out;max-width:400px;box-shadow:0 4px 16px #00000026}.notice-toast.success{background:#e6f4ea;color:#137333;border:1px solid #a8dab5}.notice-toast.error{background:#fce8e6;color:#c5221f;border:1px solid #f5c6cb}@keyframes slide-in-right{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.history-panel{position:absolute;top:100%;left:0;width:480px;max-height:360px;background:#fff;border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 30px #00000026;z-index:100;display:flex;flex-direction:column;overflow:hidden;margin-top:4px}.history-panel-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border-light);font-size:12px;font-weight:600}.history-panel-header button{background:none;border:none;cursor:pointer;font-size:14px;padding:2px 4px;border-radius:4px}.history-panel-header button:hover{background:#0000000f}.history-empty{padding:24px;text-align:center;font-size:12px;color:var(--text-muted)}.history-list{overflow-y:auto;flex:1}.history-item{padding:6px 12px;cursor:pointer;border-bottom:1px solid var(--border-light);transition:background 80ms}.history-item:hover{background:var(--select-bg-hover)}.history-item:last-child{border-bottom:none}.history-item code{font-family:var(--font-mono);font-size:11px;color:var(--text);white-space:pre-wrap;word-break:break-all;line-height:1.4}.cli-container{height:100%;display:flex;flex-direction:column;background:#fafbfc;cursor:text}.cli-output{flex:1;overflow-y:auto;padding:12px 16px;font-family:SF Mono,Monaco,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;color:#333}.cli-welcome{color:#888;padding-bottom:8px;border-bottom:1px solid #e0e0e0;margin-bottom:8px;font-size:12px}.cli-entry{margin-bottom:6px}.cli-prompt-line{display:flex;align-items:baseline}.cli-prompt{color:#2e7d32;font-weight:600;flex-shrink:0;-webkit-user-select:none;user-select:none}.cli-cmd{color:#1565c0}.cli-result{margin:2px 0 0;white-space:pre;font-family:inherit;font-size:12px;color:#444;overflow-x:auto}.cli-result.cli-error{color:#c62828}.cli-input-line{display:flex;align-items:center;padding-top:2px}.cli-input{flex:1;background:transparent;border:none;outline:none;color:#1565c0;font-family:inherit;font-size:13px;caret-color:#333}.cli-input::placeholder{color:#aaa}.cli-input:disabled{opacity:.5}.import-wizard-overlay{position:fixed;inset:0;background:var(--bg-modal-overlay);z-index:1000;display:flex;align-items:center;justify-content:center;animation:fade-in .15s ease-out}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.import-wizard-modal{background:var(--bg-main);border-radius:10px;box-shadow:0 20px 60px #0000004d;width:760px;max-width:90vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;animation:modal-in .2s ease-out}@keyframes modal-in{0%{opacity:0;transform:translateY(20px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}.import-wizard-header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border-light);flex-shrink:0}.import-wizard-title{font-size:15px;font-weight:600}.import-close-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:6px;font-size:14px;color:var(--text-secondary)}.import-close-btn:hover{background:#00000014}.import-step-indicator{display:flex;align-items:center;justify-content:center;gap:4px;padding:14px 20px 10px;flex-shrink:0}.import-step-dot{display:flex;flex-direction:column;align-items:center;gap:4px;flex:1;position:relative}.import-step-dot:after{content:"";position:absolute;top:12px;left:calc(50% + 14px);right:calc(-50% + 14px);height:2px;background:var(--border)}.import-step-dot:last-child:after{display:none}.import-step-dot.done:after{background:var(--accent)}.import-step-dot-circle{width:24px;height:24px;border-radius:50%;background:var(--border-light);color:var(--text-secondary);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;z-index:1;transition:all .2s}.import-step-dot.active .import-step-dot-circle{background:var(--accent);color:#fff;box-shadow:0 0 0 3px #2968c833}.import-step-dot.done .import-step-dot-circle{background:var(--success);color:#fff}.import-step-dot-label{font-size:11px;color:var(--text-muted);white-space:nowrap}.import-step-dot.active .import-step-dot-label{color:var(--accent);font-weight:500}.import-step-dot.done .import-step-dot-label{color:var(--success)}.import-wizard-body{flex:1;overflow-y:auto;padding:16px 24px;min-height:300px}.import-step-title{font-size:15px;font-weight:600;margin-bottom:4px}.import-step-desc{font-size:12px;color:var(--text-secondary);margin-bottom:14px}.import-dropzone{border:2px dashed var(--border);border-radius:10px;padding:32px 20px;text-align:center;cursor:pointer;transition:border-color .2s,background .2s;margin-bottom:14px}.import-dropzone:hover{border-color:var(--accent);background:#2968c80a}.import-dropzone-hint{display:flex;flex-direction:column;align-items:center;gap:6px;color:var(--text-secondary);font-size:13px}.import-file-info{display:flex;align-items:center;justify-content:center;gap:8px;font-size:13px}.import-file-icon{font-size:24px}.import-file-name{font-weight:500}.import-file-size{color:var(--text-muted);font-size:12px}.import-options-row{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);flex-wrap:wrap}.import-options-row select{padding:3px 6px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff}.import-options-row input[type=checkbox]{margin:0;accent-color:var(--accent)}.import-preview-table-wrap{max-height:340px;overflow:auto;border:1px solid var(--border-light);border-radius:6px}.import-preview-table{width:100%;border-collapse:collapse;font-size:12px}.import-preview-table th{position:sticky;top:0;background:var(--table-header-bg);padding:5px 10px;text-align:left;font-weight:500;color:var(--text-secondary);border-bottom:1px solid var(--table-border);white-space:nowrap;z-index:1}.import-preview-table td{padding:3px 10px;border-bottom:1px solid var(--border-light);white-space:nowrap;max-width:200px;overflow:hidden;text-overflow:ellipsis}.import-preview-table .import-row-num{color:var(--text-muted);text-align:center;width:36px;font-size:11px}.import-preview-table .cell-null{color:var(--text-muted);font-style:italic}.import-preview-hint{font-size:11px;color:var(--text-muted);text-align:center;padding:6px 0}.import-target-form{display:flex;flex-direction:column;gap:12px}.import-form-row{display:flex;align-items:center;gap:8px}.import-form-row label{width:80px;font-size:13px;font-weight:500;flex-shrink:0;text-align:right}.import-form-row select{flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:5px;font-size:13px;background:#fff}.import-loading{font-size:12px;color:var(--accent)}.import-mapping-table-wrap{max-height:360px;overflow:auto;border:1px solid var(--border-light);border-radius:6px}.import-mapping-table{width:100%;border-collapse:collapse;font-size:12px}.import-mapping-table th{position:sticky;top:0;background:var(--table-header-bg);padding:6px 10px;text-align:left;font-weight:500;color:var(--text-secondary);border-bottom:1px solid var(--table-border);z-index:1}.import-mapping-table td{padding:4px 10px;border-bottom:1px solid var(--border-light)}.import-mapping-table code{background:#0000000a;padding:1px 5px;border-radius:3px;font-family:var(--font-mono);font-size:12px}.import-mapping-table select{width:100%;padding:4px 6px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff}.import-mapping-skipped{opacity:.4}.import-preview-cell{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary);font-family:var(--font-mono);font-size:11px}.import-mapping-summary{font-size:12px;color:var(--text-secondary);text-align:right;padding:8px 0 0}.import-stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px}.import-stat{background:var(--bg-info);border:1px solid var(--border-light);border-radius:8px;padding:12px;text-align:center}.import-stat-label{font-size:11px;color:var(--text-secondary);margin-bottom:4px}.import-stat-value{font-size:20px;font-weight:600}.import-progress-bar-wrap{height:8px;background:var(--border-light);border-radius:4px;overflow:hidden;margin-bottom:6px}.import-progress-bar{height:100%;background:linear-gradient(90deg,var(--accent),#4fc3f7);border-radius:4px;transition:width .3s ease}.import-progress-text{font-size:12px;color:var(--text-secondary);text-align:center}.import-error-log{margin-top:14px}.import-error-list{max-height:120px;overflow-y:auto;border:1px solid #f5c6cb;border-radius:6px;background:#fef5f5;padding:8px 12px}.import-error-item{font-size:11px;color:#c62828;padding:2px 0;font-family:var(--font-mono)}.import-wizard-footer{display:flex;align-items:center;padding:12px 20px;border-top:1px solid var(--border-light);gap:8px;flex-shrink:0}.import-btn{padding:7px 18px;border-radius:6px;font-size:13px;font-weight:500;border:none;cursor:pointer;transition:background .12s}.import-btn:disabled{opacity:.4;cursor:not-allowed}.import-btn-primary{background:var(--accent);color:#fff}.import-btn-primary:hover:not(:disabled){background:var(--accent-hover)}.import-btn-secondary{background:var(--border-light);color:var(--text)}.import-btn-secondary:hover:not(:disabled){background:var(--border)}.combo-box-input{width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:5px;font-size:13px;background:#fff;outline:none;transition:border-color .15s}.combo-box-input:focus{border-color:var(--accent)}.combo-box-dropdown{position:absolute;top:100%;left:0;right:0;z-index:100;background:#fff;border:1px solid var(--border);border-top:none;border-radius:0 0 6px 6px;box-shadow:0 6px 20px #00000026;max-height:240px;overflow-y:auto}.combo-box-item{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;font-size:12px;cursor:pointer;transition:background 80ms}.combo-box-item:hover{background:var(--select-bg-hover)}.combo-box-item.selected{background:var(--select-bg);color:var(--select-text)}.combo-box-item.combo-box-new{color:var(--accent);border-bottom:1px solid var(--border-light)}.combo-box-meta{font-size:11px;color:var(--text-muted)}.combo-box-item.selected .combo-box-meta{color:#ffffffb3}.combo-box-empty{padding:8px 10px;font-size:12px;color:var(--text-muted);text-align:center}.export-step-title{font-size:15px;font-weight:600;margin-bottom:4px}.export-step-desc{font-size:12px;color:var(--text-secondary);margin-bottom:14px}.export-format-list{display:flex;flex-direction:column;gap:6px}.export-format-item{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--border-light);border-radius:8px;cursor:pointer;transition:border-color .15s,background .15s}.export-format-item:hover{border-color:var(--border);background:#00000004}.export-format-item.selected{border-color:var(--accent);background:#2968c80a}.export-format-item input[type=radio]{accent-color:var(--accent);margin:0}.export-format-info{display:flex;flex-direction:column;gap:2px}.export-format-label{font-size:13px;font-weight:500}.export-format-desc{font-size:11px;color:var(--text-muted)}.export-form-row{display:flex;align-items:center;gap:8px;margin-bottom:12px}.export-form-row label{width:70px;font-size:13px;font-weight:500;flex-shrink:0;text-align:right}.export-form-row select{flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:5px;font-size:13px;background:#fff}.export-table-search-row{display:flex;align-items:center;gap:4px;margin-bottom:6px}.export-table-search{flex:1;padding:5px 10px;border:1px solid var(--border);border-radius:5px;font-size:12px;background:#fff;outline:none}.export-table-search:focus{border-color:var(--accent)}.export-table-search-clear{padding:2px 6px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff;cursor:pointer;color:var(--text-muted)}.export-table-search-clear:hover{color:var(--danger);border-color:var(--danger)}.export-table-list-header{font-size:12px;padding:4px 0;color:var(--text-secondary);display:flex;align-items:center;gap:6px}.export-table-list-header input[type=checkbox]{accent-color:var(--accent)}.export-table-list{border:1px solid var(--border-light);border-radius:6px;max-height:280px;overflow-y:auto}.export-table-list-row{display:flex;align-items:center;gap:8px;padding:5px 10px;font-size:12px;border-bottom:1px solid var(--border-light)}.export-table-list-row:last-child{border-bottom:none}.export-table-list-head{background:var(--table-header-bg);font-weight:500;color:var(--text-secondary);position:sticky;top:0;z-index:1}.export-table-list-row.disabled{opacity:.4}.export-table-list-row input[type=checkbox]{accent-color:var(--accent);margin:0}.export-table-name{display:flex;align-items:center;gap:4px}.export-table-icon{font-size:14px}.export-filename-input{width:100%;padding:2px 6px;border:1px solid var(--border-light);border-radius:3px;font-size:12px;font-family:var(--font-mono);background:#fff}.export-filename-input:focus{border-color:var(--accent);outline:none}.export-loading{font-size:12px;color:var(--accent);padding:20px;text-align:center}.export-col-list{border:1px solid var(--border-light);border-radius:6px;max-height:300px;overflow-y:auto;padding:4px 0}.export-col-item{display:flex;align-items:center;gap:8px;padding:4px 14px;cursor:pointer;font-size:13px;transition:background .1s}.export-col-item:hover{background:var(--select-bg-hover)}.export-col-item input[type=checkbox]{accent-color:var(--accent);margin:0}.export-col-name{flex:1}.export-col-type{font-size:11px;color:var(--text-muted);font-family:var(--font-mono)}.export-col-key{font-size:12px}.export-col-actions{display:flex;align-items:center;gap:8px;padding:8px 0 0;font-size:12px}.export-col-actions button{padding:3px 10px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff;cursor:pointer}.export-col-actions button:hover{background:var(--select-bg-hover)}.export-col-actions input[type=checkbox]{accent-color:var(--accent)}.export-options-section{margin-bottom:16px}.export-options-title{font-size:13px;font-weight:600;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border-light)}.export-options-grid{display:flex;flex-direction:column;gap:8px}.export-options-grid input[type=checkbox]{accent-color:var(--accent)}.export-option-row{display:flex;align-items:center;gap:8px;font-size:13px}.export-option-row span:first-child{width:110px;text-align:right;color:var(--text-secondary);flex-shrink:0}.export-option-row select{padding:3px 8px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff}.export-stats-row{display:flex;gap:20px;margin-bottom:12px;font-size:13px}.export-stat-item{display:flex;gap:6px}.export-stat-label{font-weight:500;color:var(--text-secondary)}.export-log-box{border:1px solid var(--border-light);border-radius:6px;background:var(--bg-info);min-height:180px;max-height:280px;overflow-y:auto;padding:8px 12px;font-family:var(--font-mono);font-size:12px;margin-bottom:12px}.export-log-line{padding:2px 0;line-height:1.5}.td-container{display:flex;flex-direction:column;height:100%;background:var(--bg-main)}.td-topbar{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border);background:var(--bg-info);gap:12px;flex-shrink:0}.td-topbar-left{display:flex;align-items:center;gap:8px}.td-name-label{font-size:12px;color:var(--text-secondary);font-weight:500}.td-name-input{width:220px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;font-size:13px;font-family:var(--font-mono);background:var(--bg-main);color:var(--text)}.td-topbar-right{display:flex;gap:6px}.td-btn{padding:4px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-info);color:var(--text);font-size:12px;cursor:pointer;white-space:nowrap}.td-btn:hover{background:var(--select-bg-hover)}.td-btn:disabled{opacity:.4;cursor:default}.td-btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}.td-btn-primary:hover{background:var(--accent-hover)}.td-tab-bar{display:flex;border-bottom:1px solid var(--border);background:var(--bg-info);flex-shrink:0}.td-tab{padding:6px 16px;font-size:12px;font-weight:500;color:var(--text-secondary);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer}.td-tab:hover{color:var(--text)}.td-tab.active{color:var(--accent);border-bottom-color:var(--accent)}.td-field-toolbar{display:flex;align-items:center;gap:4px;padding:6px 12px;border-bottom:1px solid var(--border-light);background:var(--bg-info);flex-shrink:0}.td-toolbar-sep{width:1px;height:18px;background:var(--border);margin:0 4px}.td-grid-wrapper{flex:1;overflow:auto}.td-grid{width:100%;border-collapse:collapse;font-size:12px}.td-grid thead th{position:sticky;top:0;background:var(--table-header-bg);border-bottom:1px solid var(--border);padding:5px 6px;text-align:left;font-weight:600;color:var(--text-secondary);font-size:11px;white-space:nowrap;z-index:1}.td-grid tbody tr{border-bottom:1px solid var(--border-light);cursor:pointer}.td-grid tbody tr:hover{background:var(--table-row-hover)}.td-grid tbody tr.td-row-selected{background:var(--select-bg-hover)}.td-grid td{padding:1px 2px}.td-cell-num{text-align:center;color:var(--text-muted);font-size:10px;width:30px}.td-cell-input{width:100%;padding:3px 6px;border:1px solid transparent;border-radius:3px;font-size:12px;font-family:var(--font-mono);background:transparent;color:var(--text);outline:none}.td-cell-input:focus{border-color:var(--accent);background:var(--bg-main)}.td-cell-select{width:100%;padding:3px 4px;border:1px solid transparent;border-radius:3px;font-size:12px;font-family:var(--font-mono);background:transparent;color:var(--text);outline:none;cursor:pointer}.td-cell-select:focus{border-color:var(--accent);background:var(--bg-main)}.td-cell-center{text-align:center}.td-cell-short{width:100%}.td-sql-preview{flex:1;overflow:auto;padding:12px;background:var(--bg-main)}.td-sql-preview pre{margin:0;font-family:var(--font-mono);font-size:12px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-all} |
| import{g as ve}from"./_commonjsHelpers-CqkleIqs.js";function be(G,ae){for(var Q=0;Q<ae.length;Q++){const m=ae[Q];if(typeof m!="string"&&!Array.isArray(m)){for(const M in m)if(M!=="default"&&!(M in G)){const B=Object.getOwnPropertyDescriptor(m,M);B&&Object.defineProperty(G,M,B.get?B:{enumerable:!0,get:()=>m[M]})}}}return Object.freeze(Object.defineProperty(G,Symbol.toStringTag,{value:"Module"}))}var se={exports:{}};var Ee=se.exports,ge;function we(){return ge||(ge=1,(function(G,ae){((Q,m)=>{G.exports=m()})(Ee,function Q(){var m=typeof self<"u"?self:typeof window<"u"?window:m!==void 0?m:{},M,B=!m.document&&!!m.postMessage,oe=m.IS_PAPA_WORKER||!1,te={},ye=0,u={};function K(e){this._handle=null,this._finished=!1,this._completed=!1,this._halted=!1,this._input=null,this._baseIndex=0,this._partialLine="",this._rowCount=0,this._start=0,this._nextChunk=null,this.isFirstChunk=!0,this._completeResults={data:[],errors:[],meta:{}},(function(t){var r=he(t);r.chunkSize=parseInt(r.chunkSize),t.step||t.chunk||(r.chunkSize=null),this._handle=new le(r),(this._handle.streamer=this)._config=r}).call(this,e),this.parseChunk=function(t,r){var n=parseInt(this._config.skipFirstNLines)||0;if(this.isFirstChunk&&0<n){let l=this._config.newline;l||(i=this._config.quoteChar||'"',l=this._handle.guessLineEndings(t,i)),t=[...t.split(l).slice(n)].join(l)}this.isFirstChunk&&k(this._config.beforeFirstChunk)&&(i=this._config.beforeFirstChunk(t))!==void 0&&(t=i),this.isFirstChunk=!1,this._halted=!1;var n=this._partialLine+t,i=(this._partialLine="",this._handle.parse(n,this._baseIndex,!this._finished));if(!this._handle.paused()&&!this._handle.aborted()){if(t=i.meta.cursor,n=(this._finished||(this._partialLine=n.substring(t-this._baseIndex),this._baseIndex=t),i&&i.data&&(this._rowCount+=i.data.length),this._finished||this._config.preview&&this._rowCount>=this._config.preview),oe)m.postMessage({results:i,workerId:u.WORKER_ID,finished:n});else if(k(this._config.chunk)&&!r){if(this._config.chunk(i,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);this._completeResults=i=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(i.data),this._completeResults.errors=this._completeResults.errors.concat(i.errors),this._completeResults.meta=i.meta),this._completed||!n||!k(this._config.complete)||i&&i.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),n||i&&i.meta.paused||this._nextChunk(),i}this._halted=!0},this._sendError=function(t){k(this._config.error)?this._config.error(t):oe&&this._config.error&&m.postMessage({workerId:u.WORKER_ID,error:t,finished:!1})}}function re(e){var t;(e=e||{}).chunkSize||(e.chunkSize=u.RemoteChunkSize),K.call(this,e),this._nextChunk=B?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(r){this._input=r,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(t=new XMLHttpRequest,this._config.withCredentials&&(t.withCredentials=this._config.withCredentials),B||(t.onload=H(this._chunkLoaded,this),t.onerror=H(this._chunkError,this)),t.open(this._config.downloadRequestBody?"POST":"GET",this._input,!B),this._config.downloadRequestHeaders){var r,n=this._config.downloadRequestHeaders;for(r in n)t.setRequestHeader(r,n[r])}var i;this._config.chunkSize&&(i=this._start+this._config.chunkSize-1,t.setRequestHeader("Range","bytes="+this._start+"-"+i));try{t.send(this._config.downloadRequestBody)}catch(l){this._chunkError(l.message)}B&&t.status===0&&this._chunkError()}},this._chunkLoaded=function(){t.readyState===4&&(t.status<200||400<=t.status?this._chunkError():(this._start+=this._config.chunkSize||t.responseText.length,this._finished=!this._config.chunkSize||this._start>=(r=>(r=r.getResponseHeader("Content-Range"))!==null?parseInt(r.substring(r.lastIndexOf("/")+1)):-1)(t),this.parseChunk(t.responseText)))},this._chunkError=function(r){r=t.statusText||r,this._sendError(new Error(r))}}function ie(e){(e=e||{}).chunkSize||(e.chunkSize=u.LocalChunkSize),K.call(this,e);var t,r,n=typeof FileReader<"u";this.stream=function(i){this._input=i,r=i.slice||i.webkitSlice||i.mozSlice,n?((t=new FileReader).onload=H(this._chunkLoaded,this),t.onerror=H(this._chunkError,this)):t=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount<this._config.preview)||this._readChunk()},this._readChunk=function(){var i=this._input,l=(this._config.chunkSize&&(l=Math.min(this._start+this._config.chunkSize,this._input.size),i=r.call(i,this._start,l)),t.readAsText(i,this._config.encoding));n||this._chunkLoaded({target:{result:l}})},this._chunkLoaded=function(i){this._start+=this._config.chunkSize,this._finished=!this._config.chunkSize||this._start>=this._input.size,this.parseChunk(i.target.result)},this._chunkError=function(){this._sendError(t.error)}}function Y(e){var t;K.call(this,e=e||{}),this.stream=function(r){return t=r,this._nextChunk()},this._nextChunk=function(){var r,n;if(!this._finished)return r=this._config.chunkSize,t=r?(n=t.substring(0,r),t.substring(r)):(n=t,""),this._finished=!t,this.parseChunk(n)}}function ne(e){K.call(this,e=e||{});var t=[],r=!0,n=!1;this.pause=function(){K.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){K.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(i){this._input=i,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){n&&t.length===1&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):r=!0},this._streamData=H(function(i){try{t.push(typeof i=="string"?i:i.toString(this._config.encoding)),r&&(r=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(l){this._streamError(l)}},this),this._streamError=H(function(i){this._streamCleanUp(),this._sendError(i)},this),this._streamEnd=H(function(){this._streamCleanUp(),n=!0,this._streamData("")},this),this._streamCleanUp=H(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function le(e){var t,r,n,i,l=Math.pow(2,53),S=-l,F=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/,j=/^((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)))$/,d=this,E=0,a=0,T=!1,h=!1,c=[],s={data:[],errors:[],meta:{}};function x(p){return e.skipEmptyLines==="greedy"?p.join("").trim()==="":p.length===1&&p[0].length===0}function O(){if(s&&n&&(z("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+u.DefaultDelimiter+"'"),n=!1),e.skipEmptyLines&&(s.data=s.data.filter(function(o){return!x(o)})),I()){let o=function(R,C){k(e.transformHeader)&&(R=e.transformHeader(R,C)),c.push(R)};if(s)if(Array.isArray(s.data[0])){for(var p=0;I()&&p<s.data.length;p++)s.data[p].forEach(o);s.data.splice(0,1)}else s.data.forEach(o)}function g(o,R){for(var C=e.header?{}:[],y=0;y<o.length;y++){var v=y,_=o[y],_=((P,f)=>(b=>(e.dynamicTypingFunction&&e.dynamicTyping[b]===void 0&&(e.dynamicTyping[b]=e.dynamicTypingFunction(b)),(e.dynamicTyping[b]||e.dynamicTyping)===!0))(P)?f==="true"||f==="TRUE"||f!=="false"&&f!=="FALSE"&&((b=>{if(F.test(b)&&(b=parseFloat(b),S<b&&b<l))return 1})(f)?parseFloat(f):j.test(f)?new Date(f):f===""?null:f):f)(v=e.header?y>=c.length?"__parsed_extra":c[y]:v,_=e.transform?e.transform(_,v):_);v==="__parsed_extra"?(C[v]=C[v]||[],C[v].push(_)):C[v]=_}return e.header&&(y>c.length?z("FieldMismatch","TooManyFields","Too many fields: expected "+c.length+" fields but parsed "+y,a+R):y<c.length&&z("FieldMismatch","TooFewFields","Too few fields: expected "+c.length+" fields but parsed "+y,a+R)),C}var w;s&&(e.header||e.dynamicTyping||e.transform)&&(w=1,!s.data.length||Array.isArray(s.data[0])?(s.data=s.data.map(g),w=s.data.length):s.data=g(s.data,0),e.header&&s.meta&&(s.meta.fields=c),a+=w)}function I(){return e.header&&c.length===0}function z(p,g,w,o){p={type:p,code:g,message:w},o!==void 0&&(p.row=o),s.errors.push(p)}k(e.step)&&(i=e.step,e.step=function(p){s=p,I()?O():(O(),s.data.length!==0&&(E+=p.data.length,e.preview&&E>e.preview?r.abort():(s.data=s.data[0],i(s,d))))}),this.parse=function(p,g,w){var o=e.quoteChar||'"',o=(e.newline||(e.newline=this.guessLineEndings(p,o)),n=!1,e.delimiter?k(e.delimiter)&&(e.delimiter=e.delimiter(p),s.meta.delimiter=e.delimiter):((o=((R,C,y,v,_)=>{var P,f,b,$;_=_||[","," ","|",";",u.RECORD_SEP,u.UNIT_SEP];for(var J=0;J<_.length;J++){for(var U,V=_[J],D=0,q=0,A=0,L=(b=void 0,new ue({comments:v,delimiter:V,newline:C,preview:10}).parse(R)),W=0;W<L.data.length;W++)y&&x(L.data[W])?A++:(U=L.data[W].length,q+=U,b===void 0?b=U:0<U&&(D+=Math.abs(U-b),b=U));0<L.data.length&&(q/=L.data.length-A),(f===void 0||D<=f)&&($===void 0||$<q)&&1.99<q&&(f=D,P=V,$=q)}return{successful:!!(e.delimiter=P),bestDelimiter:P}})(p,e.newline,e.skipEmptyLines,e.comments,e.delimitersToGuess)).successful?e.delimiter=o.bestDelimiter:(n=!0,e.delimiter=u.DefaultDelimiter),s.meta.delimiter=e.delimiter),he(e));return e.preview&&e.header&&o.preview++,t=p,r=new ue(o),s=r.parse(t,g,w),O(),T?{meta:{paused:!0}}:s||{meta:{paused:!1}}},this.paused=function(){return T},this.pause=function(){T=!0,r.abort(),t=k(e.chunk)?"":t.substring(r.getCharIndex())},this.resume=function(){d.streamer._halted?(T=!1,d.streamer.parseChunk(t,!0)):setTimeout(d.resume,3)},this.aborted=function(){return h},this.abort=function(){h=!0,r.abort(),s.meta.aborted=!0,k(e.complete)&&e.complete(s),t=""},this.guessLineEndings=function(R,o){R=R.substring(0,1048576);var o=new RegExp(X(o)+"([^]*?)"+X(o),"gm"),w=(R=R.replace(o,"")).split("\r"),o=R.split(` | ||
| `),R=1<o.length&&o[0].length<w[0].length;if(w.length===1||R)return` | ||
| `;for(var C=0,y=0;y<w.length;y++)w[y][0]===` | ||
| `&&C++;return C>=w.length/2?`\r | ||
| `:"\r"}}function X(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function ue(e){var t=(e=e||{}).delimiter,r=e.newline,n=e.comments,i=e.step,l=e.preview,S=e.fastMode,F=null,j=!1,d=e.quoteChar==null?'"':e.quoteChar,E=d;if(e.escapeChar!==void 0&&(E=e.escapeChar),(typeof t!="string"||-1<u.BAD_DELIMITERS.indexOf(t))&&(t=","),n===t)throw new Error("Comment character same as delimiter");n===!0?n="#":(typeof n!="string"||-1<u.BAD_DELIMITERS.indexOf(n))&&(n=!1),r!==` | ||
| `&&r!=="\r"&&r!==`\r | ||
| `&&(r=` | ||
| `);var a=0,T=!1;this.parse=function(h,c,s){if(typeof h!="string")throw new Error("Input must be a string");var x=h.length,O=t.length,I=r.length,z=n.length,p=k(i),g=[],w=[],o=[],R=a=0;if(!h)return D();if(S||S!==!1&&h.indexOf(d)===-1){for(var C=h.split(r),y=0;y<C.length;y++){if(o=C[y],a+=o.length,y!==C.length-1)a+=r.length;else if(s)return D();if(!n||o.substring(0,z)!==n){if(p){if(g=[],$(o.split(t)),q(),T)return D()}else $(o.split(t));if(l&&l<=y)return g=g.slice(0,l),D(!0)}}return D()}for(var v=h.indexOf(t,a),_=h.indexOf(r,a),P=new RegExp(X(E)+X(d),"g"),f=h.indexOf(d,a);;)if(h[a]===d)for(f=a,a++;;){if((f=h.indexOf(d,f+1))===-1)return s||w.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:g.length,index:a}),U();if(f===x-1)return U(h.substring(a,f).replace(P,d));if(d===E&&h[f+1]===E)f++;else if(d===E||f===0||h[f-1]!==E){v!==-1&&v<f+1&&(v=h.indexOf(t,f+1));var b=J((_=_!==-1&&_<f+1?h.indexOf(r,f+1):_)===-1?v:Math.min(v,_));if(h.substr(f+1+b,O)===t){o.push(h.substring(a,f).replace(P,d)),h[a=f+1+b+O]!==d&&(f=h.indexOf(d,a)),v=h.indexOf(t,a),_=h.indexOf(r,a);break}if(b=J(_),h.substring(f+1+b,f+1+b+I)===r){if(o.push(h.substring(a,f).replace(P,d)),V(f+1+b+I),v=h.indexOf(t,a),f=h.indexOf(d,a),p&&(q(),T))return D();if(l&&g.length>=l)return D(!0);break}w.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:g.length,index:a}),f++}}else if(n&&o.length===0&&h.substring(a,a+z)===n){if(_===-1)return D();a=_+I,_=h.indexOf(r,a),v=h.indexOf(t,a)}else if(v!==-1&&(v<_||_===-1))o.push(h.substring(a,v)),a=v+O,v=h.indexOf(t,a);else{if(_===-1)break;if(o.push(h.substring(a,_)),V(_+I),p&&(q(),T))return D();if(l&&g.length>=l)return D(!0)}return U();function $(A){g.push(A),R=a}function J(A){var L=0;return L=A!==-1&&(A=h.substring(f+1,A))&&A.trim()===""?A.length:L}function U(A){return s||(A===void 0&&(A=h.substring(a)),o.push(A),a=x,$(o),p&&q()),D()}function V(A){a=A,$(o),o=[],_=h.indexOf(r,a)}function D(A){if(e.header&&!c&&g.length&&!j){var L=g[0],W=Object.create(null),fe=new Set(L);let pe=!1;for(let Z=0;Z<L.length;Z++){let N=L[Z];if(W[N=k(e.transformHeader)?e.transformHeader(N,Z):N]){let ee,_e=W[N];for(;ee=N+"_"+_e,_e++,fe.has(ee););fe.add(ee),L[Z]=ee,W[N]++,pe=!0,(F=F===null?{}:F)[ee]=N}else W[N]=1,L[Z]=N;fe.add(N)}pe&&console.warn("Duplicate headers found and renamed."),j=!0}return{data:g,errors:w,meta:{delimiter:t,linebreak:r,aborted:T,truncated:!!A,cursor:R+(c||0),renamedHeaders:F}}}function q(){i(D()),g=[],w=[]}},this.abort=function(){T=!0},this.getCharIndex=function(){return a}}function ke(e){var t=e.data,r=te[t.workerId],n=!1;if(t.error)r.userError(t.error,t.file);else if(t.results&&t.results.data){var i={abort:function(){n=!0,de(t.workerId,{data:[],errors:[],meta:{aborted:!0}})},pause:ce,resume:ce};if(k(r.userStep)){for(var l=0;l<t.results.data.length&&(r.userStep({data:t.results.data[l],errors:t.results.errors,meta:t.results.meta},i),!n);l++);delete t.results}else k(r.userChunk)&&(r.userChunk(t.results,i,t.file),delete t.results)}t.finished&&!n&&de(t.workerId,t.results)}function de(e,t){var r=te[e];k(r.userComplete)&&r.userComplete(t),r.terminate(),delete te[e]}function ce(){throw new Error("Not implemented.")}function he(e){if(typeof e!="object"||e===null)return e;var t,r=Array.isArray(e)?[]:{};for(t in e)r[t]=he(e[t]);return r}function H(e,t){return function(){e.apply(t,arguments)}}function k(e){return typeof e=="function"}return u.parse=function(e,t){var r=(t=t||{}).dynamicTyping||!1;if(k(r)&&(t.dynamicTypingFunction=r,r={}),t.dynamicTyping=r,t.transform=!!k(t.transform)&&t.transform,!t.worker||!u.WORKERS_SUPPORTED)return r=null,u.NODE_STREAM_INPUT,typeof e=="string"?(e=(n=>n.charCodeAt(0)!==65279?n:n.slice(1))(e),r=new(t.download?re:Y)(t)):e.readable===!0&&k(e.read)&&k(e.on)?r=new ne(t):(m.File&&e instanceof File||e instanceof Object)&&(r=new ie(t)),r.stream(e);(r=(()=>{var n;return!!u.WORKERS_SUPPORTED&&(n=(()=>{var i=m.URL||m.webkitURL||null,l=Q.toString();return u.BLOB_URL||(u.BLOB_URL=i.createObjectURL(new Blob(["var global = (function() { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } return {}; })(); global.IS_PAPA_WORKER=true; ","(",l,")();"],{type:"text/javascript"})))})(),(n=new m.Worker(n)).onmessage=ke,n.id=ye++,te[n.id]=n)})()).userStep=t.step,r.userChunk=t.chunk,r.userComplete=t.complete,r.userError=t.error,t.step=k(t.step),t.chunk=k(t.chunk),t.complete=k(t.complete),t.error=k(t.error),delete t.worker,r.postMessage({input:e,config:t,workerId:r.id})},u.unparse=function(e,t){var r=!1,n=!0,i=",",l=`\r | ||
| `,S='"',F=S+S,j=!1,d=null,E=!1,a=((()=>{if(typeof t=="object"){if(typeof t.delimiter!="string"||u.BAD_DELIMITERS.filter(function(c){return t.delimiter.indexOf(c)!==-1}).length||(i=t.delimiter),typeof t.quotes!="boolean"&&typeof t.quotes!="function"&&!Array.isArray(t.quotes)||(r=t.quotes),typeof t.skipEmptyLines!="boolean"&&typeof t.skipEmptyLines!="string"||(j=t.skipEmptyLines),typeof t.newline=="string"&&(l=t.newline),typeof t.quoteChar=="string"&&(S=t.quoteChar),typeof t.header=="boolean"&&(n=t.header),Array.isArray(t.columns)){if(t.columns.length===0)throw new Error("Option columns is empty");d=t.columns}t.escapeChar!==void 0&&(F=t.escapeChar+S),t.escapeFormulae instanceof RegExp?E=t.escapeFormulae:typeof t.escapeFormulae=="boolean"&&t.escapeFormulae&&(E=/^[=+\-@\t\r].*$/)}})(),new RegExp(X(S),"g"));if(typeof e=="string"&&(e=JSON.parse(e)),Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return T(null,e,j);if(typeof e[0]=="object")return T(d||Object.keys(e[0]),e,j)}else if(typeof e=="object")return typeof e.data=="string"&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields||d),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:typeof e.data[0]=="object"?Object.keys(e.data[0]):[]),Array.isArray(e.data[0])||typeof e.data[0]=="object"||(e.data=[e.data])),T(e.fields||[],e.data||[],j);throw new Error("Unable to serialize unrecognized input");function T(c,s,x){var O="",I=(typeof c=="string"&&(c=JSON.parse(c)),typeof s=="string"&&(s=JSON.parse(s)),Array.isArray(c)&&0<c.length),z=!Array.isArray(s[0]);if(I&&n){for(var p=0;p<c.length;p++)0<p&&(O+=i),O+=h(c[p],p);0<s.length&&(O+=l)}for(var g=0;g<s.length;g++){var w=(I?c:s[g]).length,o=!1,R=I?Object.keys(s[g]).length===0:s[g].length===0;if(x&&!I&&(o=x==="greedy"?s[g].join("").trim()==="":s[g].length===1&&s[g][0].length===0),x==="greedy"&&I){for(var C=[],y=0;y<w;y++){var v=z?c[y]:y;C.push(s[g][v])}o=C.join("").trim()===""}if(!o){for(var _=0;_<w;_++){0<_&&!R&&(O+=i);var P=I&&z?c[_]:_;O+=h(s[g][P],_)}g<s.length-1&&(!x||0<w&&!R)&&(O+=l)}}return O}function h(c,s){var x,O;return c==null?"":c.constructor===Date?JSON.stringify(c).slice(1,25):(O=!1,E&&typeof c=="string"&&E.test(c)&&(c="'"+c,O=!0),x=c.toString().replace(a,F),(O=O||r===!0||typeof r=="function"&&r(c,s)||Array.isArray(r)&&r[s]||((I,z)=>{for(var p=0;p<z.length;p++)if(-1<I.indexOf(z[p]))return!0;return!1})(x,u.BAD_DELIMITERS)||-1<x.indexOf(i)||x.charAt(0)===" "||x.charAt(x.length-1)===" ")?S+x+S:x)}},u.RECORD_SEP="",u.UNIT_SEP="",u.BYTE_ORDER_MARK="\uFEFF",u.BAD_DELIMITERS=["\r",` | ||
| `,'"',u.BYTE_ORDER_MARK],u.WORKERS_SUPPORTED=!B&&!!m.Worker,u.NODE_STREAM_INPUT=1,u.LocalChunkSize=10485760,u.RemoteChunkSize=5242880,u.DefaultDelimiter=",",u.Parser=ue,u.ParserHandle=le,u.NetworkStreamer=re,u.FileStreamer=ie,u.StringStreamer=Y,u.ReadableStreamStreamer=ne,m.jQuery&&((M=m.jQuery).fn.parse=function(e){var t=e.config||{},r=[];return this.each(function(l){if(!(M(this).prop("tagName").toUpperCase()==="INPUT"&&M(this).attr("type").toLowerCase()==="file"&&m.FileReader)||!this.files||this.files.length===0)return!0;for(var S=0;S<this.files.length;S++)r.push({file:this.files[S],inputElem:this,instanceConfig:M.extend({},t)})}),n(),this;function n(){if(r.length===0)k(e.complete)&&e.complete();else{var l,S,F,j,d=r[0];if(k(e.before)){var E=e.before(d.file,d.inputElem);if(typeof E=="object"){if(E.action==="abort")return l="AbortError",S=d.file,F=d.inputElem,j=E.reason,void(k(e.error)&&e.error({name:l},S,F,j));if(E.action==="skip")return void i();typeof E.config=="object"&&(d.instanceConfig=M.extend(d.instanceConfig,E.config))}else if(E==="skip")return void i()}var a=d.instanceConfig.complete;d.instanceConfig.complete=function(T){k(a)&&a(T,d.file,d.inputElem),i()},u.parse(d.file,d.instanceConfig)}}function i(){r.splice(0,1),n()}}),oe&&(m.onmessage=function(e){e=e.data,u.WORKER_ID===void 0&&e&&(u.WORKER_ID=e.workerId),typeof e.input=="string"?m.postMessage({workerId:u.WORKER_ID,results:u.parse(e.input,e.config),finished:!0}):(m.File&&e.input instanceof File||e.input instanceof Object)&&(e=u.parse(e.input,e.config))&&m.postMessage({workerId:u.WORKER_ID,results:e,finished:!0})}),(re.prototype=Object.create(K.prototype)).constructor=re,(ie.prototype=Object.create(K.prototype)).constructor=ie,(Y.prototype=Object.create(Y.prototype)).constructor=Y,(ne.prototype=Object.create(K.prototype)).constructor=ne,u})})(se)),se.exports}var me=we();const Re=ve(me),Se=be({__proto__:null,default:Re},[me]);export{Se as p}; |
Sorry, the diff of this file is too big to display
| import type { TableColumn } from './types' | ||
| type CursorValueMap = Record<string, unknown> | ||
| function q(identifier: string) { | ||
| return `\`${identifier.replaceAll('`', '``')}\`` | ||
| } | ||
| function sqlLiteral(value: unknown) { | ||
| if (value === null || value === undefined) return 'NULL' | ||
| if (typeof value === 'number' || typeof value === 'bigint') return String(value) | ||
| if (typeof value === 'boolean') return value ? '1' : '0' | ||
| return `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "''")}'` | ||
| } | ||
| export function getPrimaryKeyColumns(columns: Pick<TableColumn, 'name' | 'key'>[]) { | ||
| return columns | ||
| .filter((column) => column.key === 'PRI') | ||
| .map((column) => column.name) | ||
| } | ||
| export function readPrimaryKeyCursor( | ||
| row: Record<string, unknown>, | ||
| primaryKeyColumns: string[], | ||
| ): CursorValueMap | null { | ||
| if (primaryKeyColumns.length === 0) { | ||
| return null | ||
| } | ||
| const cursor: CursorValueMap = {} | ||
| for (const column of primaryKeyColumns) { | ||
| if (!(column in row)) { | ||
| return null | ||
| } | ||
| cursor[column] = row[column] | ||
| } | ||
| return cursor | ||
| } | ||
| export function buildBatchSelectQuery(options: { | ||
| batchSize: number | ||
| baseWhereClause?: string | ||
| fallbackOffset: number | ||
| primaryKeyColumns: string[] | ||
| schemaName: string | ||
| selectColumns: string[] | ||
| tableName: string | ||
| cursor?: CursorValueMap | null | ||
| }) { | ||
| const { | ||
| batchSize, | ||
| baseWhereClause, | ||
| cursor, | ||
| fallbackOffset, | ||
| primaryKeyColumns, | ||
| schemaName, | ||
| selectColumns, | ||
| tableName, | ||
| } = options | ||
| const queryColumns = Array.from(new Set([ | ||
| ...selectColumns, | ||
| ...primaryKeyColumns, | ||
| ])) | ||
| if (primaryKeyColumns.length > 0) { | ||
| const orderClause = primaryKeyColumns | ||
| .map((column) => `${q(column)} ASC`) | ||
| .join(', ') | ||
| const filters: string[] = [] | ||
| if (baseWhereClause) { | ||
| filters.push(`(${baseWhereClause})`) | ||
| } | ||
| if (cursor) { | ||
| filters.push(`(${primaryKeyColumns.map((column) => q(column)).join(', ')}) > (${primaryKeyColumns.map((column) => sqlLiteral(cursor[column])).join(', ')})`) | ||
| } | ||
| const whereClause = filters.length > 0 | ||
| ? ` WHERE ${filters.join(' AND ')}` | ||
| : '' | ||
| return { | ||
| mode: 'primaryKey' as const, | ||
| queryColumns, | ||
| sql: `SELECT ${queryColumns.map((column) => q(column)).join(', ')} FROM ${q(schemaName)}.${q(tableName)}${whereClause} ORDER BY ${orderClause} LIMIT ${batchSize}`, | ||
| } | ||
| } | ||
| return { | ||
| mode: 'offset' as const, | ||
| queryColumns, | ||
| sql: `SELECT ${queryColumns.map((column) => q(column)).join(', ')} FROM ${q(schemaName)}.${q(tableName)}${baseWhereClause ? ` WHERE ${baseWhereClause}` : ''} LIMIT ${batchSize} OFFSET ${fallbackOffset}`, | ||
| } | ||
| } |
+2
-2
@@ -8,4 +8,4 @@ <!doctype html> | ||
| <title>NaviDog</title> | ||
| <script type="module" crossorigin src="/assets/index-D_lCxgd0.js"></script> | ||
| <link rel="stylesheet" crossorigin href="/assets/index-BnM_oG_z.css"> | ||
| <script type="module" crossorigin src="/assets/index-DFmUIsKl.js"></script> | ||
| <link rel="stylesheet" crossorigin href="/assets/index-DJykcpp2.css"> | ||
| </head> | ||
@@ -12,0 +12,0 @@ <body> |
+2
-1
| { | ||
| "name": "navidog", | ||
| "version": "1.2.0", | ||
| "version": "1.2.1", | ||
| "description": "🐕 轻量级 Web 端 MySQL 管理工具,风格类似 Navicat", | ||
@@ -58,2 +58,3 @@ "repository": { | ||
| "react-dom": "^19.2.0", | ||
| "sql-formatter": "^15.7.2", | ||
| "ssh2": "^1.17.0", | ||
@@ -60,0 +61,0 @@ "tsx": "^4.20.6", |
+64
-2
@@ -6,8 +6,12 @@ import express from 'express' | ||
| import { | ||
| cancelActiveQuery, | ||
| closePools, | ||
| disposeConnection, | ||
| executeSql, | ||
| executeStatements, | ||
| fetchSchemaTree, | ||
| fetchTableColumns, | ||
| fetchTableMetadata, | ||
| normalizeConnectionPayload, | ||
| QueryCancelledError, | ||
| testConnection, | ||
@@ -83,2 +87,21 @@ } from './mysql.js' | ||
| app.post('/api/schema/table-meta', async (request, response) => { | ||
| try { | ||
| const connection = normalizeConnectionPayload(request.body.connection) | ||
| const schemaName = String(request.body.schemaName ?? '').trim() | ||
| const tableName = String(request.body.tableName ?? '').trim() | ||
| if (!schemaName || !tableName) { | ||
| throw new Error('Schema and table are required.') | ||
| } | ||
| const metadata = await fetchTableMetadata(connection, schemaName, tableName) | ||
| response.json(metadata) | ||
| } catch (error) { | ||
| response.status(400).json({ | ||
| error: error instanceof Error ? error.message : 'Could not inspect table metadata.', | ||
| }) | ||
| } | ||
| }) | ||
| app.post('/api/query', async (request, response) => { | ||
@@ -89,4 +112,5 @@ try { | ||
| const database = request.body.database ? String(request.body.database).trim() : undefined | ||
| const queryId = request.body.queryId ? String(request.body.queryId).trim() : undefined | ||
| const startedAt = Date.now() | ||
| const results = await executeSql(connection, sql, database) | ||
| const results = await executeSql(connection, sql, database, queryId) | ||
| response.json({ | ||
@@ -98,3 +122,3 @@ durationMs: Date.now() - startedAt, | ||
| console.error('[/api/query] ERROR') | ||
| response.status(400).json({ | ||
| response.status(error instanceof QueryCancelledError ? 409 : 400).json({ | ||
| error: error instanceof Error ? error.message : 'SQL execution failed.', | ||
@@ -105,2 +129,40 @@ }) | ||
| app.post('/api/query/batch', async (request, response) => { | ||
| try { | ||
| const connection = normalizeConnectionPayload(request.body.connection) | ||
| const statements = Array.isArray(request.body.statements) | ||
| ? request.body.statements.map((statement: unknown) => String(statement ?? '')) | ||
| : [] | ||
| const database = request.body.database ? String(request.body.database).trim() : undefined | ||
| const transaction = request.body.transaction !== false | ||
| const startedAt = Date.now() | ||
| const results = await executeStatements(connection, statements, database, transaction) | ||
| response.json({ | ||
| durationMs: Date.now() - startedAt, | ||
| results, | ||
| }) | ||
| } catch (error) { | ||
| console.error('[/api/query/batch] ERROR') | ||
| response.status(400).json({ | ||
| error: error instanceof Error ? error.message : 'Batch SQL execution failed.', | ||
| }) | ||
| } | ||
| }) | ||
| app.post('/api/query/cancel', async (request, response) => { | ||
| try { | ||
| const queryId = String(request.body.queryId ?? '').trim() | ||
| if (!queryId) { | ||
| throw new Error('Query ID is required.') | ||
| } | ||
| const ok = await cancelActiveQuery(queryId) | ||
| response.json({ ok }) | ||
| } catch (error) { | ||
| response.status(400).json({ | ||
| error: error instanceof Error ? error.message : 'Could not cancel query.', | ||
| }) | ||
| } | ||
| }) | ||
| /* ── Data Import ────────────────────────────── */ | ||
@@ -107,0 +169,0 @@ app.post('/api/import/batch', async (request, response) => { |
+304
-20
@@ -5,2 +5,3 @@ import crypto from 'node:crypto' | ||
| type Pool, | ||
| type PoolConnection, | ||
| type ResultSetHeader, | ||
@@ -68,2 +69,22 @@ type RowDataPacket, | ||
| export type TableIndexInfo = { | ||
| name: string | ||
| unique: boolean | ||
| primary: boolean | ||
| type: string | ||
| columns: string[] | ||
| cardinality?: number | null | ||
| comment?: string | ||
| } | ||
| export type TableForeignKeyInfo = { | ||
| name: string | ||
| columns: string[] | ||
| referencedSchema: string | ||
| referencedTable: string | ||
| referencedColumns: string[] | ||
| onUpdate: string | ||
| onDelete: string | ||
| } | ||
| export type QueryResultSet = | ||
@@ -95,4 +116,10 @@ | { | ||
| type ActiveQueryRecord = { | ||
| cancelled: boolean | ||
| connection: PoolConnection & { destroy?: () => void; connection?: { destroy?: () => void } } | ||
| } | ||
| const pools = new Map<string, Pool>() | ||
| const pendingPools = new Map<string, Promise<Pool>>() | ||
| const activeQueries = new Map<string, ActiveQueryRecord>() | ||
| const hiddenSchemas = new Set([ | ||
@@ -105,2 +132,18 @@ 'information_schema', | ||
| export class QueryCancelledError extends Error { | ||
| constructor() { | ||
| super('Query was cancelled.') | ||
| this.name = 'QueryCancelledError' | ||
| } | ||
| } | ||
| function destroyPoolConnection(connection: ActiveQueryRecord['connection']) { | ||
| if (typeof connection.destroy === 'function') { | ||
| connection.destroy() | ||
| return | ||
| } | ||
| connection.connection?.destroy?.() | ||
| } | ||
| function readBoolean(value: unknown, fallback: boolean) { | ||
@@ -517,2 +560,8 @@ if (typeof value === 'boolean') { | ||
| export async function closePools() { | ||
| for (const [, activeQuery] of activeQueries) { | ||
| activeQuery.cancelled = true | ||
| destroyPoolConnection(activeQuery.connection) | ||
| } | ||
| activeQueries.clear() | ||
| await Promise.all(Array.from(pools.values(), (pool) => pool.end())) | ||
@@ -626,2 +675,135 @@ pools.clear() | ||
| export async function fetchTableMetadata( | ||
| config: ConnectionConfig, | ||
| schemaName: string, | ||
| tableName: string, | ||
| ): Promise<{ indexes: TableIndexInfo[]; foreignKeys: TableForeignKeyInfo[] }> { | ||
| return withSslFallback(config, (effectiveConfig) => | ||
| withReconnect(effectiveConfig, async () => { | ||
| const pool = await getPool(effectiveConfig) | ||
| const [indexRows] = await pool.query<RowDataPacket[]>( | ||
| `SHOW INDEX FROM ${q(schemaName)}.${q(tableName)}`, | ||
| ) | ||
| const indexesByName = new Map<string, { | ||
| name: string | ||
| unique: boolean | ||
| primary: boolean | ||
| type: string | ||
| cardinality?: number | null | ||
| comment?: string | ||
| columns: { seq: number; name: string }[] | ||
| }>() | ||
| for (const row of indexRows) { | ||
| const indexName = String(row.Key_name ?? '') | ||
| if (!indexName) continue | ||
| const existing = indexesByName.get(indexName) ?? { | ||
| name: indexName, | ||
| unique: Number(row.Non_unique ?? 1) === 0, | ||
| primary: indexName === 'PRIMARY', | ||
| type: String(row.Index_type ?? ''), | ||
| cardinality: row.Cardinality != null ? Number(row.Cardinality) : null, | ||
| comment: row.Index_comment ? String(row.Index_comment) : undefined, | ||
| columns: [], | ||
| } | ||
| existing.columns.push({ | ||
| seq: Number(row.Seq_in_index ?? existing.columns.length + 1), | ||
| name: String(row.Column_name ?? ''), | ||
| }) | ||
| indexesByName.set(indexName, existing) | ||
| } | ||
| const indexes = Array.from(indexesByName.values()) | ||
| .map((index) => ({ | ||
| name: index.name, | ||
| unique: index.unique, | ||
| primary: index.primary, | ||
| type: index.type, | ||
| columns: index.columns | ||
| .sort((a, b) => a.seq - b.seq) | ||
| .map((column) => column.name), | ||
| cardinality: index.cardinality ?? null, | ||
| comment: index.comment, | ||
| })) | ||
| .sort((a, b) => { | ||
| if (a.primary && !b.primary) return -1 | ||
| if (!a.primary && b.primary) return 1 | ||
| return a.name.localeCompare(b.name) | ||
| }) | ||
| const [fkRows] = await pool.query<RowDataPacket[]>( | ||
| ` | ||
| SELECT | ||
| kcu.CONSTRAINT_NAME AS constraintName, | ||
| kcu.COLUMN_NAME AS columnName, | ||
| kcu.ORDINAL_POSITION AS ordinalPosition, | ||
| kcu.REFERENCED_TABLE_SCHEMA AS referencedSchema, | ||
| kcu.REFERENCED_TABLE_NAME AS referencedTable, | ||
| kcu.REFERENCED_COLUMN_NAME AS referencedColumn, | ||
| rc.UPDATE_RULE AS updateRule, | ||
| rc.DELETE_RULE AS deleteRule | ||
| FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu | ||
| INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc | ||
| ON rc.CONSTRAINT_SCHEMA = kcu.TABLE_SCHEMA | ||
| AND rc.TABLE_NAME = kcu.TABLE_NAME | ||
| AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME | ||
| WHERE kcu.TABLE_SCHEMA = ? | ||
| AND kcu.TABLE_NAME = ? | ||
| AND kcu.REFERENCED_TABLE_NAME IS NOT NULL | ||
| ORDER BY kcu.CONSTRAINT_NAME, kcu.ORDINAL_POSITION | ||
| `, | ||
| [schemaName, tableName], | ||
| ) | ||
| const foreignKeysByName = new Map<string, { | ||
| name: string | ||
| referencedSchema: string | ||
| referencedTable: string | ||
| onUpdate: string | ||
| onDelete: string | ||
| columns: { seq: number; name: string }[] | ||
| referencedColumns: { seq: number; name: string }[] | ||
| }>() | ||
| for (const row of fkRows) { | ||
| const name = String(row.constraintName ?? '') | ||
| if (!name) continue | ||
| const existing = foreignKeysByName.get(name) ?? { | ||
| name, | ||
| referencedSchema: String(row.referencedSchema ?? schemaName), | ||
| referencedTable: String(row.referencedTable ?? ''), | ||
| onUpdate: String(row.updateRule ?? 'RESTRICT'), | ||
| onDelete: String(row.deleteRule ?? 'RESTRICT'), | ||
| columns: [], | ||
| referencedColumns: [], | ||
| } | ||
| const seq = Number(row.ordinalPosition ?? existing.columns.length + 1) | ||
| existing.columns.push({ seq, name: String(row.columnName ?? '') }) | ||
| existing.referencedColumns.push({ seq, name: String(row.referencedColumn ?? '') }) | ||
| foreignKeysByName.set(name, existing) | ||
| } | ||
| const foreignKeys = Array.from(foreignKeysByName.values()).map((fk) => ({ | ||
| name: fk.name, | ||
| columns: fk.columns.sort((a, b) => a.seq - b.seq).map((column) => column.name), | ||
| referencedSchema: fk.referencedSchema, | ||
| referencedTable: fk.referencedTable, | ||
| referencedColumns: fk.referencedColumns | ||
| .sort((a, b) => a.seq - b.seq) | ||
| .map((column) => column.name), | ||
| onUpdate: fk.onUpdate, | ||
| onDelete: fk.onDelete, | ||
| })) | ||
| return { indexes, foreignKeys } | ||
| }), | ||
| ) | ||
| } | ||
| function isResultSetHeader(value: unknown): value is ResultSetHeader { | ||
@@ -681,2 +863,29 @@ return value !== null && typeof value === 'object' && 'affectedRows' in value | ||
| function normalizeQueryPayload( | ||
| rows: unknown, | ||
| fields: FieldPacket[] | FieldPacket[][] | undefined, | ||
| startIndex = 0, | ||
| ): QueryResultSet[] { | ||
| if ( | ||
| Array.isArray(rows) && | ||
| rows.some((entry) => Array.isArray(entry) || isResultSetHeader(entry)) | ||
| ) { | ||
| return rows.map((entry, index) => { | ||
| const fieldSet = | ||
| Array.isArray(fields) && Array.isArray(fields[index]) | ||
| ? (fields[index] as FieldPacket[]) | ||
| : undefined | ||
| return normalizeSingleResult(entry, fieldSet, startIndex + index) | ||
| }) | ||
| } | ||
| const singleFieldSet = | ||
| Array.isArray(fields) && fields.length > 0 && !Array.isArray(fields[0]) | ||
| ? (fields as FieldPacket[]) | ||
| : undefined | ||
| return [normalizeSingleResult(rows, singleFieldSet, startIndex)] | ||
| } | ||
| export async function executeSql( | ||
@@ -686,2 +895,3 @@ config: ConnectionConfig, | ||
| database?: string, | ||
| queryId?: string, | ||
| ): Promise<QueryResultSet[]> { | ||
@@ -697,4 +907,11 @@ const trimmedSql = sql.trim() | ||
| const pool = await getPool(effectiveConfig) | ||
| const conn = await pool.getConnection() | ||
| const conn = await pool.getConnection() as PoolConnection & { destroy?: () => void; connection?: { destroy?: () => void } } | ||
| const activeQuery = queryId | ||
| ? { cancelled: false, connection: conn } | ||
| : null | ||
| if (queryId && activeQuery) { | ||
| activeQueries.set(queryId, activeQuery) | ||
| } | ||
| try { | ||
@@ -706,28 +923,95 @@ if (database) { | ||
| const [rows, fields] = await conn.query(trimmedSql) | ||
| return normalizeQueryPayload(rows, fields) | ||
| } catch (error) { | ||
| if (activeQuery?.cancelled) { | ||
| throw new QueryCancelledError() | ||
| } | ||
| throw error | ||
| } finally { | ||
| if (queryId) { | ||
| activeQueries.delete(queryId) | ||
| } | ||
| try { | ||
| conn.release() | ||
| } catch { | ||
| // Destroyed connections may already be closed. | ||
| } | ||
| } | ||
| }), | ||
| ) | ||
| } | ||
| if ( | ||
| Array.isArray(rows) && | ||
| rows.some((entry) => Array.isArray(entry) || isResultSetHeader(entry)) | ||
| ) { | ||
| return rows.map((entry, index) => { | ||
| const fieldSet = | ||
| Array.isArray(fields) && Array.isArray(fields[index]) | ||
| ? (fields[index] as FieldPacket[]) | ||
| : undefined | ||
| export async function executeStatements( | ||
| config: ConnectionConfig, | ||
| statements: string[], | ||
| database?: string, | ||
| transaction = false, | ||
| ): Promise<QueryResultSet[]> { | ||
| const trimmedStatements = statements | ||
| .map((statement) => statement.trim()) | ||
| .filter(Boolean) | ||
| return normalizeSingleResult(entry, fieldSet, index) | ||
| }) | ||
| if (trimmedStatements.length === 0) { | ||
| throw new Error('No SQL statements provided.') | ||
| } | ||
| return withSslFallback(config, async (effectiveConfig) => { | ||
| const pool = await getPool(effectiveConfig) | ||
| const conn = await pool.getConnection() | ||
| let transactionStarted = false | ||
| try { | ||
| if (database) { | ||
| await conn.query(`USE \`${database.replaceAll('`', '``')}\``) | ||
| } | ||
| if (transaction) { | ||
| await conn.beginTransaction() | ||
| transactionStarted = true | ||
| } | ||
| const results: QueryResultSet[] = [] | ||
| for (let index = 0; index < trimmedStatements.length; index += 1) { | ||
| const statement = trimmedStatements[index] | ||
| try { | ||
| const [rows, fields] = await conn.query(statement) | ||
| results.push(...normalizeQueryPayload(rows, fields, results.length)) | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : String(error) | ||
| throw new Error(`Statement ${index + 1} failed: ${message}`) | ||
| } | ||
| } | ||
| const singleFieldSet = | ||
| Array.isArray(fields) && fields.length > 0 && !Array.isArray(fields[0]) | ||
| ? (fields as FieldPacket[]) | ||
| : undefined | ||
| if (transactionStarted) { | ||
| await conn.commit() | ||
| transactionStarted = false | ||
| } | ||
| return [normalizeSingleResult(rows, singleFieldSet, 0)] | ||
| } finally { | ||
| return results | ||
| } catch (error) { | ||
| if (transactionStarted) { | ||
| await conn.rollback().catch(() => undefined) | ||
| } | ||
| throw formatConnectionError(effectiveConfig, error) | ||
| } finally { | ||
| try { | ||
| conn.release() | ||
| } catch { | ||
| // Destroyed connections may already be closed. | ||
| } | ||
| }), | ||
| ) | ||
| } | ||
| }) | ||
| } | ||
| export async function cancelActiveQuery(queryId: string): Promise<boolean> { | ||
| const activeQuery = activeQueries.get(queryId) | ||
| if (!activeQuery) { | ||
| return false | ||
| } | ||
| activeQuery.cancelled = true | ||
| destroyPoolConnection(activeQuery.connection) | ||
| return true | ||
| } |
+26
-2
@@ -7,2 +7,3 @@ import type { | ||
| TableColumnsResponse, | ||
| TableMetadataResponse, | ||
| TestConnectionResponse, | ||
@@ -47,5 +48,28 @@ } from './types' | ||
| }, | ||
| runQuery(connection: ConnectionProfile, sql: string, database?: string) { | ||
| return request<QueryExecutionResponse>('/query', { connection, sql, database }) | ||
| fetchTableMetadata(connection: ConnectionProfile, schemaName: string, tableName: string) { | ||
| return request<TableMetadataResponse>('/schema/table-meta', { | ||
| connection, | ||
| schemaName, | ||
| tableName, | ||
| }) | ||
| }, | ||
| runQuery(connection: ConnectionProfile, sql: string, database?: string, queryId?: string) { | ||
| return request<QueryExecutionResponse>('/query', { connection, sql, database, queryId }) | ||
| }, | ||
| runStatements( | ||
| connection: ConnectionProfile, | ||
| statements: string[], | ||
| database?: string, | ||
| transaction = true, | ||
| ) { | ||
| return request<QueryExecutionResponse>('/query/batch', { | ||
| connection, | ||
| statements, | ||
| database, | ||
| transaction, | ||
| }) | ||
| }, | ||
| cancelQuery(queryId: string) { | ||
| return request<{ ok: boolean }>('/query/cancel', { queryId }) | ||
| }, | ||
| importBatch( | ||
@@ -52,0 +76,0 @@ connection: ConnectionProfile, |
| import { useEffect, useRef, useState } from 'react' | ||
| import * as XLSX from 'xlsx' | ||
| import { api } from '../api' | ||
| import { | ||
| buildBatchSelectQuery, | ||
| getPrimaryKeyColumns, | ||
| readPrimaryKeyCursor, | ||
| } from '../sqlPaging' | ||
| import type { ConnectionProfile, SchemaNode, TableColumn } from '../types' | ||
@@ -24,2 +28,98 @@ | ||
| type SavePickerWindow = Window & { | ||
| showSaveFilePicker?: (options: { | ||
| suggestedName: string | ||
| types: Array<{ | ||
| description: string | ||
| accept: Record<string, string[]> | ||
| }> | ||
| }) => Promise<FileSystemFileHandle> | ||
| } | ||
| type TextExportTarget = { | ||
| kind: 'download' | 'file' | ||
| write: (chunk: string) => Promise<void> | ||
| close: () => Promise<Blob | null> | ||
| } | ||
| function escapeCsvValue( | ||
| value: unknown, | ||
| separator: string, | ||
| qualifier: string, | ||
| ) { | ||
| if (value === null || value === undefined) return '' | ||
| const text = String(value) | ||
| if (!qualifier) { | ||
| return text | ||
| } | ||
| if ( | ||
| text.includes(separator) || | ||
| text.includes(qualifier) || | ||
| text.includes('\n') || | ||
| text.includes('\r') | ||
| ) { | ||
| const escapedQualifier = qualifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') | ||
| return `${qualifier}${text.replace(new RegExp(escapedQualifier, 'g'), qualifier + qualifier)}${qualifier}` | ||
| } | ||
| return text | ||
| } | ||
| function toSqlLiteral(value: unknown) { | ||
| if (value === null || value === undefined) return 'NULL' | ||
| if (typeof value === 'number' || typeof value === 'bigint') return String(value) | ||
| if (typeof value === 'boolean') return value ? '1' : '0' | ||
| return `'${String(value) | ||
| .replace(/\\/g, '\\\\') | ||
| .replace(/'/g, "\\'") | ||
| .replace(/\n/g, '\\n') | ||
| .replace(/\r/g, '\\r') | ||
| .replace(/\t/g, '\\t')}'` | ||
| } | ||
| async function createTextExportTarget( | ||
| fileName: string, | ||
| mimeType: string, | ||
| useFileSystemAccess: boolean, | ||
| ): Promise<TextExportTarget> { | ||
| if (useFileSystemAccess) { | ||
| const pickerWindow = window as SavePickerWindow | ||
| const handle = await pickerWindow.showSaveFilePicker?.({ | ||
| suggestedName: fileName, | ||
| types: [{ | ||
| description: 'Export Files', | ||
| accept: { [mimeType]: [`.${fileName.split('.').pop() ?? 'txt'}`] }, | ||
| }], | ||
| }) | ||
| if (handle) { | ||
| const writable = await handle.createWritable() | ||
| return { | ||
| kind: 'file', | ||
| async write(chunk) { | ||
| await writable.write(chunk) | ||
| }, | ||
| async close() { | ||
| await writable.close() | ||
| return null | ||
| }, | ||
| } | ||
| } | ||
| } | ||
| const parts: BlobPart[] = [] | ||
| return { | ||
| kind: 'download', | ||
| async write(chunk) { | ||
| parts.push(chunk) | ||
| }, | ||
| async close() { | ||
| return new Blob(parts, { type: mimeType }) | ||
| }, | ||
| } | ||
| } | ||
| type Props = { | ||
@@ -137,2 +237,4 @@ connection: ConnectionProfile | ||
| } | ||
| // `loadColumns` is intentionally not included to avoid reloading on every render. | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [step, activeTable]) | ||
@@ -199,3 +301,9 @@ | ||
| const allFiles: { name: string; blob: Blob }[] = [] | ||
| const useFileSystemAccess = | ||
| enabledTables.length === 1 && | ||
| format !== 'xlsx' && | ||
| typeof window !== 'undefined' && | ||
| 'showSaveFilePicker' in window | ||
| let totalProcessed = 0 | ||
| let completedExports = 0 | ||
@@ -207,2 +315,3 @@ for (let ti = 0; ti < enabledTables.length; ti++) { | ||
| const activeCols = cols.filter((c) => selCols.has(c.name)) | ||
| const fileName = tc.fileName.trim() || `${tc.name}.${FORMAT_EXT[format]}` | ||
@@ -217,3 +326,2 @@ if (activeCols.length === 0) { | ||
| const BATCH = 5000 | ||
| const MAX_ROWS = 500000 | ||
| let offset = 0 | ||
@@ -223,99 +331,161 @@ let hasMore = true | ||
| const colNames = activeCols.map((c) => c.name) | ||
| const primaryKeyColumns = getPrimaryKeyColumns(cols) | ||
| let cursor: Record<string, unknown> | null = null | ||
| let blob: Blob | null = null | ||
| // Collect all rows for this table | ||
| const allRows: Record<string, unknown>[] = [] | ||
| try { | ||
| if (format === 'xlsx') { | ||
| const XLSX = await import('xlsx') | ||
| const MAX_XLSX_ROWS = 500000 | ||
| const wsData: unknown[][] = [] | ||
| if (includeHeader) { | ||
| wsData.push(colNames) | ||
| } | ||
| setExportLog((prev) => [...prev, `⚠️ ${tc.name}: xlsx 导出会在浏览器内存中组装文件,大表建议改用 CSV / SQL`]) | ||
| while (hasMore) { | ||
| try { | ||
| const colList = colNames.map((c) => `\`${c.replace(/`/g, '``')}\``).join(', ') | ||
| const sql = `SELECT ${colList} FROM \`${selectedSchema}\`.\`${tc.name}\` LIMIT ${BATCH} OFFSET ${offset}` | ||
| const result = await api.runQuery(connection, sql, selectedSchema) | ||
| const dataResult = result.results?.[0] | ||
| while (hasMore) { | ||
| const { mode, sql } = buildBatchSelectQuery({ | ||
| schemaName: selectedSchema, | ||
| tableName: tc.name, | ||
| selectColumns: colNames, | ||
| primaryKeyColumns, | ||
| batchSize: BATCH, | ||
| fallbackOffset: offset, | ||
| cursor, | ||
| }) | ||
| const result = await api.runQuery(connection, sql, selectedSchema) | ||
| const dataResult = result.results?.[0] | ||
| if (dataResult?.kind === 'rows' && dataResult.rows.length > 0) { | ||
| allRows.push(...dataResult.rows) | ||
| rowCount += dataResult.rows.length | ||
| offset += dataResult.rows.length | ||
| if (dataResult.rows.length < BATCH) hasMore = false | ||
| if (rowCount >= MAX_ROWS) { | ||
| setExportLog((prev) => [...prev, `⚠️ ${tc.name}: 达到 ${MAX_ROWS} 行限制`]) | ||
| if (dataResult?.kind === 'rows' && dataResult.rows.length > 0) { | ||
| for (const row of dataResult.rows) { | ||
| wsData.push(colNames.map((c) => row[c] ?? '')) | ||
| } | ||
| rowCount += dataResult.rows.length | ||
| if (mode === 'primaryKey') { | ||
| cursor = readPrimaryKeyCursor(dataResult.rows[dataResult.rows.length - 1], primaryKeyColumns) | ||
| } else { | ||
| offset += dataResult.rows.length | ||
| } | ||
| totalProcessed += dataResult.rows.length | ||
| setProgress((prev) => ({ ...prev, processed: totalProcessed, tables: ti })) | ||
| if (dataResult.rows.length < BATCH) { | ||
| hasMore = false | ||
| } | ||
| if (rowCount >= MAX_XLSX_ROWS) { | ||
| setExportLog((prev) => [...prev, `⚠️ ${tc.name}: xlsx 达到 ${MAX_XLSX_ROWS.toLocaleString()} 行限制`]) | ||
| hasMore = false | ||
| } | ||
| } else { | ||
| hasMore = false | ||
| } | ||
| } | ||
| const ws = XLSX.utils.aoa_to_sheet(wsData) | ||
| const wb = XLSX.utils.book_new() | ||
| XLSX.utils.book_append_sheet(wb, ws, tc.name.slice(0, 31)) | ||
| const xlsxData = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }) as ArrayBuffer | ||
| blob = new Blob([xlsxData], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) | ||
| } else { | ||
| const mimeType = format === 'json' | ||
| ? 'application/json;charset=utf-8' | ||
| : 'text/plain;charset=utf-8' | ||
| const target = await createTextExportTarget(fileName, mimeType, useFileSystemAccess) | ||
| let firstJsonRow = true | ||
| if (format === 'csv' || format === 'txt') { | ||
| if (includeHeader) { | ||
| await target.write(colNames.map((c) => escapeCsvValue(c, delimiter, textQualifier)).join(delimiter) + eol) | ||
| } | ||
| } else if (format === 'json') { | ||
| await target.write('[') | ||
| } else { | ||
| hasMore = false | ||
| await target.write(`-- Export of ${tc.name}${eol}-- Date: ${new Date().toISOString()}${eol}${eol}`) | ||
| } | ||
| } catch (err) { | ||
| setExportLog((prev) => [...prev, `❌ ${tc.name}: ${err instanceof Error ? err.message : String(err)}`]) | ||
| hasMore = false | ||
| } | ||
| totalProcessed += Math.min(BATCH, rowCount - (offset - BATCH)) | ||
| setProgress((prev) => ({ ...prev, processed: totalProcessed, tables: ti })) | ||
| } | ||
| // Generate file content based on format | ||
| let blob: Blob | ||
| while (hasMore) { | ||
| const { mode, sql } = buildBatchSelectQuery({ | ||
| schemaName: selectedSchema, | ||
| tableName: tc.name, | ||
| selectColumns: colNames, | ||
| primaryKeyColumns, | ||
| batchSize: BATCH, | ||
| fallbackOffset: offset, | ||
| cursor, | ||
| }) | ||
| const result = await api.runQuery(connection, sql, selectedSchema) | ||
| const dataResult = result.results?.[0] | ||
| if (format === 'csv' || format === 'txt') { | ||
| const sep = delimiter | ||
| let fileContent = '' | ||
| if (includeHeader) { | ||
| fileContent += colNames.map((c) => `${textQualifier}${c}${textQualifier}`).join(sep) + eol | ||
| } | ||
| for (const row of allRows) { | ||
| const vals = colNames.map((c) => { | ||
| const v = row[c] | ||
| if (v === null || v === undefined) return '' | ||
| const s = String(v) | ||
| if (s.includes(sep) || s.includes(textQualifier) || s.includes('\n') || s.includes('\r')) { | ||
| return `${textQualifier}${s.replace(new RegExp(textQualifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), textQualifier + textQualifier)}${textQualifier}` | ||
| if (dataResult?.kind === 'rows' && dataResult.rows.length > 0) { | ||
| rowCount += dataResult.rows.length | ||
| if (mode === 'primaryKey') { | ||
| cursor = readPrimaryKeyCursor(dataResult.rows[dataResult.rows.length - 1], primaryKeyColumns) | ||
| } else { | ||
| offset += dataResult.rows.length | ||
| } | ||
| totalProcessed += dataResult.rows.length | ||
| setProgress((prev) => ({ ...prev, processed: totalProcessed, tables: ti })) | ||
| let chunk = '' | ||
| if (format === 'csv' || format === 'txt') { | ||
| chunk = dataResult.rows.map((row) => | ||
| colNames | ||
| .map((columnName) => escapeCsvValue(row[columnName], delimiter, textQualifier)) | ||
| .join(delimiter), | ||
| ).join(eol) | ||
| if (chunk) { | ||
| chunk += eol | ||
| } | ||
| } else if (format === 'json') { | ||
| chunk = dataResult.rows.map((row) => { | ||
| const obj: Record<string, unknown> = {} | ||
| for (const columnName of colNames) { | ||
| obj[columnName] = row[columnName] ?? null | ||
| } | ||
| const prefix = firstJsonRow ? `${eol} ` : `,${eol} ` | ||
| firstJsonRow = false | ||
| return prefix + JSON.stringify(obj) | ||
| }).join('') | ||
| } else { | ||
| chunk = dataResult.rows.map((row) => | ||
| `INSERT INTO \`${tc.name}\` (${colNames.map((columnName) => `\`${columnName}\``).join(', ')}) VALUES (${colNames.map((columnName) => toSqlLiteral(row[columnName])).join(', ')});`, | ||
| ).join(eol) | ||
| if (chunk) { | ||
| chunk += eol | ||
| } | ||
| } | ||
| if (chunk) { | ||
| await target.write(chunk) | ||
| } | ||
| if (dataResult.rows.length < BATCH) { | ||
| hasMore = false | ||
| } | ||
| } else { | ||
| hasMore = false | ||
| } | ||
| return s | ||
| }) | ||
| fileContent += vals.join(sep) + eol | ||
| } | ||
| blob = new Blob([fileContent], { type: 'text/plain;charset=utf-8' }) | ||
| } | ||
| } else if (format === 'xlsx') { | ||
| // Build worksheet data | ||
| const wsData: unknown[][] = [] | ||
| if (includeHeader) { | ||
| wsData.push(colNames) | ||
| if (format === 'json') { | ||
| await target.write((firstJsonRow ? '' : eol) + ']' + eol) | ||
| } | ||
| blob = await target.close() | ||
| } | ||
| for (const row of allRows) { | ||
| wsData.push(colNames.map((c) => row[c] ?? '')) | ||
| } catch (err) { | ||
| if (err instanceof DOMException && err.name === 'AbortError') { | ||
| setExportLog((prev) => [...prev, '⏹️ 已取消导出']) | ||
| setExporting(false) | ||
| return | ||
| } | ||
| const ws = XLSX.utils.aoa_to_sheet(wsData) | ||
| const wb = XLSX.utils.book_new() | ||
| XLSX.utils.book_append_sheet(wb, ws, tc.name.slice(0, 31)) // Sheet name max 31 chars | ||
| const xlsxData = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }) as ArrayBuffer | ||
| blob = new Blob([xlsxData], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) | ||
| } else if (format === 'json') { | ||
| let fileContent = '[' | ||
| let first = true | ||
| for (const row of allRows) { | ||
| const obj: Record<string, unknown> = {} | ||
| for (const c of colNames) { obj[c] = row[c] ?? null } | ||
| if (!first) fileContent += ',' | ||
| fileContent += eol + ' ' + JSON.stringify(obj) | ||
| first = false | ||
| } | ||
| fileContent += eol + ']' + eol | ||
| blob = new Blob([fileContent], { type: 'application/json;charset=utf-8' }) | ||
| setExportLog((prev) => [...prev, `❌ ${tc.name}: ${err instanceof Error ? err.message : String(err)}`]) | ||
| continue | ||
| } | ||
| } else { | ||
| // SQL | ||
| let fileContent = `-- Export of ${tc.name}${eol}-- Date: ${new Date().toISOString()}${eol}${eol}` | ||
| for (const row of allRows) { | ||
| const vals = colNames.map((c) => { | ||
| const v = row[c] | ||
| if (v === null || v === undefined) return 'NULL' | ||
| return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'` | ||
| }) | ||
| fileContent += `INSERT INTO \`${tc.name}\` (${colNames.map((c) => `\`${c}\``).join(', ')}) VALUES (${vals.join(', ')});${eol}` | ||
| } | ||
| blob = new Blob([fileContent], { type: 'text/plain;charset=utf-8' }) | ||
| if (blob) { | ||
| allFiles.push({ name: fileName, blob }) | ||
| } | ||
| allFiles.push({ name: tc.fileName, blob }) | ||
| completedExports += 1 | ||
| setExportLog((prev) => [...prev, `✅ ${tc.name}: ${rowCount.toLocaleString()} 行`]) | ||
@@ -335,3 +505,3 @@ setProgress((prev) => ({ ...prev, total: prev.total + rowCount, tables: ti + 1 })) | ||
| setExporting(false) | ||
| onFlash('success', `导出完成: ${allFiles.length} 个文件`) | ||
| onFlash('success', `导出完成: ${completedExports} 个文件`) | ||
| } | ||
@@ -338,0 +508,0 @@ |
| import { useEffect, useRef, useState } from 'react' | ||
| import Papa from 'papaparse' | ||
| import * as XLSX from 'xlsx' | ||
| import { api } from '../api' | ||
@@ -210,2 +208,3 @@ import type { ConnectionProfile, ImportCreateTablePlan, SchemaNode, TableColumn } from '../types' | ||
| const [fileType, setFileType] = useState<'csv' | 'json' | 'excel' | 'sql'>('csv') | ||
| const [parsingFile, setParsingFile] = useState(false) | ||
@@ -279,57 +278,68 @@ /* Step 2: Target */ | ||
| /* ── Parse file ────────────────────────── */ | ||
| function parseFile() { | ||
| async function parseFile() { | ||
| if (!rawFile) return | ||
| setParsingFile(true) | ||
| if (fileType === 'json') { | ||
| parseJsonFile() | ||
| } else if (fileType === 'excel') { | ||
| parseExcelFile() | ||
| await parseExcelFile() | ||
| } else if (fileType === 'sql') { | ||
| parseSqlFile() | ||
| } else { | ||
| parseCsvFile() | ||
| await parseCsvFile() | ||
| } | ||
| } | ||
| function parseCsvFile() { | ||
| async function parseCsvFile() { | ||
| if (!rawFile) return | ||
| Papa.parse(rawFile, { | ||
| delimiter: delimiter === 'auto' ? undefined : delimiter, | ||
| header: hasHeader, | ||
| encoding, | ||
| skipEmptyLines: true, | ||
| dynamicTyping: true, | ||
| complete: (results) => { | ||
| if (hasHeader) { | ||
| const fields = results.meta.fields ?? [] | ||
| setSourceFields(fields) | ||
| const rows = results.data as Record<string, unknown>[] | ||
| setAllParsedRows(rows) | ||
| setTotalRowCount(rows.length) | ||
| const preview = rows.slice(0, 20).map((row) => | ||
| fields.map((f) => { | ||
| const v = row[f] | ||
| return v === null || v === undefined ? '' : String(v) | ||
| }), | ||
| ) | ||
| setPreviewRows(preview) | ||
| } else { | ||
| const data = results.data as string[][] | ||
| const fields = data[0]?.map((_, i) => `Column ${i + 1}`) ?? [] | ||
| setSourceFields(fields) | ||
| const rows = data.map((row) => { | ||
| const obj: Record<string, unknown> = {} | ||
| fields.forEach((f, i) => { obj[f] = row[i] }) | ||
| return obj | ||
| }) | ||
| setAllParsedRows(rows) | ||
| setTotalRowCount(rows.length) | ||
| setPreviewRows(data.slice(0, 20)) | ||
| } | ||
| setStep(1) | ||
| }, | ||
| error: (err) => { | ||
| onFlash('error', `解析失败: ${err.message}`) | ||
| }, | ||
| }) | ||
| try { | ||
| const { default: Papa } = await import('papaparse') | ||
| Papa.parse(rawFile, { | ||
| delimiter: delimiter === 'auto' ? undefined : delimiter, | ||
| header: hasHeader, | ||
| encoding, | ||
| skipEmptyLines: true, | ||
| dynamicTyping: true, | ||
| complete: (results) => { | ||
| if (hasHeader) { | ||
| const fields = results.meta.fields ?? [] | ||
| setSourceFields(fields) | ||
| const rows = results.data as Record<string, unknown>[] | ||
| setAllParsedRows(rows) | ||
| setTotalRowCount(rows.length) | ||
| const preview = rows.slice(0, 20).map((row) => | ||
| fields.map((f) => { | ||
| const v = row[f] | ||
| return v === null || v === undefined ? '' : String(v) | ||
| }), | ||
| ) | ||
| setPreviewRows(preview) | ||
| } else { | ||
| const data = results.data as string[][] | ||
| const fields = data[0]?.map((_, i) => `Column ${i + 1}`) ?? [] | ||
| setSourceFields(fields) | ||
| const rows = data.map((row) => { | ||
| const obj: Record<string, unknown> = {} | ||
| fields.forEach((f, i) => { obj[f] = row[i] }) | ||
| return obj | ||
| }) | ||
| setAllParsedRows(rows) | ||
| setTotalRowCount(rows.length) | ||
| setPreviewRows(data.slice(0, 20)) | ||
| } | ||
| setParsingFile(false) | ||
| setStep(1) | ||
| }, | ||
| error: (err) => { | ||
| setParsingFile(false) | ||
| onFlash('error', `解析失败: ${err.message}`) | ||
| }, | ||
| }) | ||
| } catch (err) { | ||
| setParsingFile(false) | ||
| onFlash('error', `加载 CSV 解析器失败: ${err instanceof Error ? err.message : String(err)}`) | ||
| } | ||
| } | ||
@@ -349,2 +359,3 @@ | ||
| else { | ||
| setParsingFile(false) | ||
| onFlash('error', 'JSON 文件必须是数组格式') | ||
@@ -355,2 +366,3 @@ return | ||
| if (data.length === 0) { | ||
| setParsingFile(false) | ||
| onFlash('error', 'JSON 文件为空') | ||
@@ -370,42 +382,62 @@ return | ||
| setPreviewRows(preview) | ||
| setParsingFile(false) | ||
| setStep(1) | ||
| } catch (err) { | ||
| setParsingFile(false) | ||
| onFlash('error', `JSON 解析失败: ${err instanceof Error ? err.message : String(err)}`) | ||
| } | ||
| } | ||
| reader.onerror = () => { | ||
| setParsingFile(false) | ||
| onFlash('error', '读取 JSON 文件失败') | ||
| } | ||
| reader.readAsText(rawFile, encoding) | ||
| } | ||
| function parseExcelFile() { | ||
| async function parseExcelFile() { | ||
| if (!rawFile) return | ||
| const reader = new FileReader() | ||
| reader.onload = (e) => { | ||
| try { | ||
| const data = new Uint8Array(e.target?.result as ArrayBuffer) | ||
| const workbook = XLSX.read(data, { type: 'array' }) | ||
| const sheetName = workbook.SheetNames[0] | ||
| const sheet = workbook.Sheets[sheetName] | ||
| const jsonData = XLSX.utils.sheet_to_json<Record<string, unknown>>(sheet, { defval: '' }) | ||
| if (jsonData.length === 0) { | ||
| onFlash('error', 'Excel 文件为空') | ||
| return | ||
| try { | ||
| const XLSX = await import('xlsx') | ||
| const reader = new FileReader() | ||
| reader.onload = (e) => { | ||
| try { | ||
| const data = new Uint8Array(e.target?.result as ArrayBuffer) | ||
| const workbook = XLSX.read(data, { type: 'array' }) | ||
| const sheetName = workbook.SheetNames[0] | ||
| const sheet = workbook.Sheets[sheetName] | ||
| const jsonData = XLSX.utils.sheet_to_json<Record<string, unknown>>(sheet, { defval: '' }) | ||
| if (jsonData.length === 0) { | ||
| setParsingFile(false) | ||
| onFlash('error', 'Excel 文件为空') | ||
| return | ||
| } | ||
| const fields = Object.keys(jsonData[0]) | ||
| setSourceFields(fields) | ||
| setAllParsedRows(jsonData) | ||
| setTotalRowCount(jsonData.length) | ||
| const preview = jsonData.slice(0, 20).map((row) => | ||
| fields.map((f) => { | ||
| const v = row[f] | ||
| return v === null || v === undefined ? '' : String(v) | ||
| }), | ||
| ) | ||
| setPreviewRows(preview) | ||
| setParsingFile(false) | ||
| setStep(1) | ||
| } catch (err) { | ||
| setParsingFile(false) | ||
| onFlash('error', `Excel 解析失败: ${err instanceof Error ? err.message : String(err)}`) | ||
| } | ||
| const fields = Object.keys(jsonData[0]) | ||
| setSourceFields(fields) | ||
| setAllParsedRows(jsonData) | ||
| setTotalRowCount(jsonData.length) | ||
| const preview = jsonData.slice(0, 20).map((row) => | ||
| fields.map((f) => { | ||
| const v = row[f] | ||
| return v === null || v === undefined ? '' : String(v) | ||
| }), | ||
| ) | ||
| setPreviewRows(preview) | ||
| setStep(1) | ||
| } catch (err) { | ||
| onFlash('error', `Excel 解析失败: ${err instanceof Error ? err.message : String(err)}`) | ||
| } | ||
| reader.onerror = () => { | ||
| setParsingFile(false) | ||
| onFlash('error', '读取 Excel 文件失败') | ||
| } | ||
| reader.readAsArrayBuffer(rawFile) | ||
| } catch (err) { | ||
| setParsingFile(false) | ||
| onFlash('error', `加载 Excel 解析器失败: ${err instanceof Error ? err.message : String(err)}`) | ||
| } | ||
| reader.readAsArrayBuffer(rawFile) | ||
| } | ||
@@ -442,2 +474,3 @@ | ||
| if (rows.length === 0) { | ||
| setParsingFile(false) | ||
| onFlash('error', 'SQL 文件中未找到 INSERT 语句') | ||
@@ -456,7 +489,13 @@ return | ||
| setPreviewRows(preview) | ||
| setParsingFile(false) | ||
| setStep(1) | ||
| } catch (err) { | ||
| setParsingFile(false) | ||
| onFlash('error', `SQL 解析失败: ${err instanceof Error ? err.message : String(err)}`) | ||
| } | ||
| } | ||
| reader.onerror = () => { | ||
| setParsingFile(false) | ||
| onFlash('error', '读取 SQL 文件失败') | ||
| } | ||
| reader.readAsText(rawFile, encoding) | ||
@@ -951,3 +990,3 @@ } | ||
| if (step === 0) { | ||
| parseFile() | ||
| void parseFile() | ||
| return | ||
@@ -1061,6 +1100,6 @@ } | ||
| className="import-btn import-btn-primary" | ||
| disabled={!canNext} | ||
| onClick={handleNext} | ||
| disabled={!canNext || parsingFile} | ||
| onClick={() => void handleNext()} | ||
| > | ||
| {step === 3 ? '开始导入' : '下一步 →'} | ||
| {step === 0 && parsingFile ? '解析中...' : step === 3 ? '开始导入' : '下一步 →'} | ||
| </button> | ||
@@ -1067,0 +1106,0 @@ )} |
+450
-97
@@ -1,91 +0,135 @@ | ||
| import { useEffect, useRef, useMemo } from 'react' | ||
| import { useEffect, useRef, useMemo, useImperativeHandle, forwardRef } from 'react' | ||
| import { EditorView, keymap, placeholder as cmPlaceholder } from '@codemirror/view' | ||
| import { EditorState, Compartment } from '@codemirror/state' | ||
| import { sql, MySQL, type SQLConfig } from '@codemirror/lang-sql' | ||
| import { EditorState, Compartment, Prec } from '@codemirror/state' | ||
| import { sql, MySQL, keywordCompletionSource } from '@codemirror/lang-sql' | ||
| import { basicSetup } from 'codemirror' | ||
| import { defaultKeymap, indentWithTab } from '@codemirror/commands' | ||
| import { autocompletion } from '@codemirror/autocomplete' | ||
| import { | ||
| acceptCompletion, | ||
| autocompletion, | ||
| completionKeymap, | ||
| completionStatus, | ||
| type Completion, | ||
| type CompletionContext, | ||
| } from '@codemirror/autocomplete' | ||
| import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' | ||
| import { tags } from '@lezer/highlight' | ||
| import type { SchemaNode } from '../types' | ||
| /* ═══════════════════════════════════════════════ | ||
| Light theme – matches NaviDog IDE style | ||
| Dark syntax highlighting – VS Code Dark+ palette | ||
| (matches the right panel DDL colors) | ||
| ═══════════════════════════════════════════════ */ | ||
| const darkHighlightStyle = HighlightStyle.define([ | ||
| { tag: tags.keyword, color: '#569cd6' }, | ||
| { tag: tags.operatorKeyword, color: '#569cd6' }, | ||
| { tag: tags.definitionKeyword, color: '#569cd6' }, | ||
| { tag: tags.typeName, color: '#4ec9b0' }, | ||
| { tag: tags.string, color: '#ce9178' }, | ||
| { tag: tags.number, color: '#b5cea8' }, | ||
| { tag: tags.bool, color: '#569cd6' }, | ||
| { tag: tags.null, color: '#569cd6' }, | ||
| { tag: tags.comment, color: '#6a9955' }, | ||
| { tag: tags.lineComment, color: '#6a9955' }, | ||
| { tag: tags.blockComment, color: '#6a9955' }, | ||
| { tag: tags.punctuation, color: '#808080' }, | ||
| { tag: tags.paren, color: '#808080' }, | ||
| { tag: tags.squareBracket, color: '#808080' }, | ||
| { tag: tags.brace, color: '#808080' }, | ||
| { tag: tags.operator, color: '#d4d4d4' }, | ||
| { tag: tags.variableName, color: '#9cdcfe' }, | ||
| { tag: tags.propertyName, color: '#9cdcfe' }, | ||
| ]) | ||
| const lightHighlightStyle = HighlightStyle.define([ | ||
| { tag: tags.keyword, color: '#0033b3' }, | ||
| { tag: tags.operatorKeyword, color: '#0033b3' }, | ||
| { tag: tags.definitionKeyword, color: '#0033b3' }, | ||
| { tag: tags.typeName, color: '#067d17' }, | ||
| { tag: tags.string, color: '#a5060e' }, | ||
| { tag: tags.number, color: '#1750eb' }, | ||
| { tag: tags.bool, color: '#0033b3' }, | ||
| { tag: tags.null, color: '#0033b3' }, | ||
| { tag: tags.comment, color: '#8c8c8c' }, | ||
| { tag: tags.lineComment, color: '#8c8c8c' }, | ||
| { tag: tags.blockComment, color: '#8c8c8c' }, | ||
| { tag: tags.punctuation, color: '#333' }, | ||
| { tag: tags.paren, color: '#333' }, | ||
| { tag: tags.squareBracket, color: '#333' }, | ||
| { tag: tags.brace, color: '#333' }, | ||
| { tag: tags.operator, color: '#333' }, | ||
| { tag: tags.variableName, color: '#871094' }, | ||
| { tag: tags.propertyName, color: '#871094' }, | ||
| ]) | ||
| /* ═══════════════════════════════════════════════ | ||
| Editor base theme | ||
| ═══════════════════════════════════════════════ */ | ||
| const navidogTheme = EditorView.theme({ | ||
| '&': { | ||
| fontSize: '13px', | ||
| height: '100%', | ||
| fontSize: '13px', | ||
| fontFamily: "'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace", | ||
| background: '#ffffff', | ||
| }, | ||
| '.cm-content': { | ||
| caretColor: '#2968c8', | ||
| padding: '8px 0', | ||
| '.cm-scroller': { | ||
| fontFamily: 'var(--font-mono)', | ||
| overflow: 'auto', | ||
| }, | ||
| '.cm-cursor': { | ||
| borderLeftColor: '#2968c8', | ||
| '.cm-gutters': { | ||
| backgroundColor: 'transparent', | ||
| border: 'none', | ||
| paddingRight: '4px', | ||
| }, | ||
| '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': { | ||
| background: '#cce2ff !important', | ||
| '.cm-lineNumbers .cm-gutterElement': { | ||
| paddingLeft: '8px', | ||
| paddingRight: '6px', | ||
| minWidth: '24px', | ||
| color: '#b0b0b0', | ||
| fontSize: '12px', | ||
| }, | ||
| '.cm-activeLine': { | ||
| background: '#f5f8ff', | ||
| backgroundColor: 'rgba(0,0,0,0.03)', | ||
| }, | ||
| '.cm-gutters': { | ||
| background: '#f8f8f8', | ||
| borderRight: '1px solid #e2e2e2', | ||
| color: '#aaa', | ||
| fontSize: '11px', | ||
| }, | ||
| '.cm-activeLineGutter': { | ||
| background: '#eef3fb', | ||
| color: '#333', | ||
| backgroundColor: 'transparent', | ||
| }, | ||
| /* Autocomplete tooltip */ | ||
| '.cm-tooltip': { | ||
| border: '1px solid rgba(0,0,0,0.12)', | ||
| borderRadius: '6px', | ||
| boxShadow: '0 6px 24px rgba(0,0,0,0.15)', | ||
| background: 'rgba(255,255,255,0.97)', | ||
| backdropFilter: 'blur(12px)', | ||
| overflow: 'hidden', | ||
| '.cm-cursor': { | ||
| borderLeftColor: '#333', | ||
| }, | ||
| '.cm-tooltip.cm-tooltip-autocomplete': { | ||
| '& > ul': { | ||
| fontFamily: "'SF Mono', 'Menlo', monospace", | ||
| fontSize: '12px', | ||
| maxHeight: '240px', | ||
| }, | ||
| '& > ul > li': { | ||
| padding: '3px 10px 3px 6px', | ||
| lineHeight: '1.5', | ||
| }, | ||
| '& > ul > li[aria-selected]': { | ||
| background: '#2968c8', | ||
| color: '#fff', | ||
| }, | ||
| '.cm-selectionBackground': { | ||
| backgroundColor: 'rgba(41,104,200,0.2) !important', | ||
| }, | ||
| '.cm-completionIcon': { | ||
| width: '18px', | ||
| paddingRight: '4px', | ||
| opacity: '0.7', | ||
| '&.cm-focused .cm-selectionBackground': { | ||
| backgroundColor: 'rgba(41,104,200,0.3) !important', | ||
| }, | ||
| '.cm-tooltip-autocomplete': { | ||
| maxHeight: '280px', | ||
| fontSize: '12px', | ||
| border: '1px solid #d0d0d0', | ||
| borderRadius: '6px', | ||
| boxShadow: '0 4px 14px rgba(0,0,0,0.12)', | ||
| }, | ||
| '.cm-completionLabel': { | ||
| fontWeight: '500', | ||
| fontSize: '12px', | ||
| }, | ||
| '.cm-completionDetail': { | ||
| marginLeft: '8px', | ||
| fontSize: '11px', | ||
| fontStyle: 'normal', | ||
| color: '#999', | ||
| fontSize: '11px', | ||
| marginLeft: '8px', | ||
| }, | ||
| /* Search panel */ | ||
| '.cm-completionIcon': { | ||
| fontSize: '12px', | ||
| opacity: '0.7', | ||
| }, | ||
| '.cm-tooltip-autocomplete ul li[aria-selected]': { | ||
| backgroundColor: 'rgba(41,104,200,0.12)', | ||
| color: 'inherit', | ||
| }, | ||
| '.cm-panels': { | ||
| borderBottom: '1px solid #e2e2e2', | ||
| background: '#f8f8f8', | ||
| backgroundColor: 'transparent', | ||
| }, | ||
| /* Matching brackets */ | ||
| '.cm-matchingBracket': { | ||
| background: '#d4edbc', | ||
| outline: '1px solid #92c353', | ||
| '.cm-search': { | ||
| fontSize: '13px', | ||
| }, | ||
@@ -95,3 +139,3 @@ }) | ||
| /* ═══════════════════════════════════════════════ | ||
| Props | ||
| Exported types | ||
| ═══════════════════════════════════════════════ */ | ||
@@ -103,2 +147,3 @@ | ||
| onRun?: () => void | ||
| onRunSelection?: (selectedSql: string) => void | ||
| schemas?: SchemaNode[] | ||
@@ -108,4 +153,14 @@ currentSchema?: string | ||
| placeholder?: string | ||
| suppressExternalValueSync?: boolean | ||
| } | ||
| export type SqlEditorHandle = { | ||
| getSelectedText: () => string | ||
| getValue: () => string | ||
| getRunnableSql: () => string | ||
| setValue: (value: string) => void | ||
| formatSql: () => void | ||
| getView: () => EditorView | null | ||
| } | ||
| /* ═══════════════════════════════════════════════ | ||
@@ -115,9 +170,145 @@ Component | ||
| // Compartment for dynamic SQL language reconfiguration | ||
| const sqlCompartment = new Compartment() | ||
| const highlightCompartment = new Compartment() | ||
| const autocompleteCompartment = new Compartment() | ||
| export default function SqlEditor({ | ||
| function isDark() { | ||
| return document.documentElement.getAttribute('data-theme') === 'dark' | ||
| } | ||
| async function formatSqlDocument(sqlText: string) { | ||
| const { format } = await import('sql-formatter') | ||
| return format(sqlText, { | ||
| language: 'mysql', | ||
| tabWidth: 2, | ||
| keywordCase: 'upper', | ||
| }) | ||
| } | ||
| /** Check if the semicolon at `index` is a real statement separator (not inside a string/comment) */ | ||
| function isStatementSeparator(sqlText: string, index: number) { | ||
| let inSingle = false | ||
| let inDouble = false | ||
| let inBacktick = false | ||
| let inLineComment = false | ||
| let inBlockComment = false | ||
| for (let i = 0; i <= index; i += 1) { | ||
| const ch = sqlText[i] | ||
| const next = sqlText[i + 1] | ||
| const prev = sqlText[i - 1] | ||
| if (inLineComment) { | ||
| if (ch === '\n') inLineComment = false | ||
| continue | ||
| } | ||
| if (inBlockComment) { | ||
| if (prev === '*' && ch === '/') inBlockComment = false | ||
| continue | ||
| } | ||
| if (inSingle) { | ||
| if (ch === "'" && prev !== '\\') inSingle = false | ||
| continue | ||
| } | ||
| if (inDouble) { | ||
| if (ch === '"' && prev !== '\\') inDouble = false | ||
| continue | ||
| } | ||
| if (inBacktick) { | ||
| if (ch === '`') inBacktick = false | ||
| continue | ||
| } | ||
| if (ch === '-' && next === '-') { | ||
| inLineComment = true | ||
| i += 1 | ||
| continue | ||
| } | ||
| if (ch === '#') { | ||
| inLineComment = true | ||
| continue | ||
| } | ||
| if (ch === '/' && next === '*') { | ||
| inBlockComment = true | ||
| i += 1 | ||
| continue | ||
| } | ||
| if (ch === "'") { | ||
| inSingle = true | ||
| continue | ||
| } | ||
| if (ch === '"') { | ||
| inDouble = true | ||
| continue | ||
| } | ||
| if (ch === '`') { | ||
| inBacktick = true | ||
| continue | ||
| } | ||
| if (ch === ';' && i === index) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
| /** Find the boundaries of the SQL statement surrounding the cursor position */ | ||
| function getStatementBoundaries(sqlText: string, cursor: number) { | ||
| if (!sqlText.trim()) { | ||
| return { from: 0, to: sqlText.length } | ||
| } | ||
| let from = 0 | ||
| for (let i = Math.min(cursor - 1, sqlText.length - 1); i >= 0; i -= 1) { | ||
| if (sqlText[i] === ';' && isStatementSeparator(sqlText, i)) { | ||
| from = i + 1 | ||
| break | ||
| } | ||
| } | ||
| let to = sqlText.length | ||
| for (let i = Math.max(0, cursor); i < sqlText.length; i += 1) { | ||
| if (sqlText[i] === ';' && isStatementSeparator(sqlText, i)) { | ||
| to = i + 1 | ||
| break | ||
| } | ||
| } | ||
| return { from, to } | ||
| } | ||
| /** Get the SQL that should be run: selection if present, else current statement, else all */ | ||
| function getRunnableSqlFromView(view: EditorView) { | ||
| const { from, to, head } = view.state.selection.main | ||
| if (from !== to) { | ||
| return view.state.sliceDoc(from, to).trim() | ||
| } | ||
| const sqlText = view.state.doc.toString() | ||
| const current = getStatementBoundaries(sqlText, head) | ||
| const activeSql = sqlText.slice(current.from, current.to).trim() | ||
| if (activeSql) { | ||
| return activeSql | ||
| } | ||
| return sqlText.trim() | ||
| } | ||
| const SqlEditor = forwardRef<SqlEditorHandle, SqlEditorProps>(function SqlEditor({ | ||
| value, | ||
| onChange, | ||
| onRun, | ||
| onRunSelection, | ||
| schemas = [], | ||
@@ -127,3 +318,4 @@ currentSchema, | ||
| placeholder = '在这里输入 SQL...', | ||
| }: SqlEditorProps) { | ||
| suppressExternalValueSync = false, | ||
| }, ref) { | ||
| const containerRef = useRef<HTMLDivElement>(null) | ||
@@ -133,32 +325,125 @@ const viewRef = useRef<EditorView | null>(null) | ||
| const onRunRef = useRef(onRun) | ||
| const onRunSelectionRef = useRef(onRunSelection) | ||
| const lastEmittedRef = useRef(value) | ||
| // Keep refs current | ||
| onChangeRef.current = onChange | ||
| onRunRef.current = onRun | ||
| onRunSelectionRef.current = onRunSelection | ||
| // Build SQL schema config for autocomplete | ||
| const sqlConfig = useMemo<SQLConfig>(() => { | ||
| const schemaMap: Record<string, string[]> = {} | ||
| useImperativeHandle(ref, () => ({ | ||
| getSelectedText() { | ||
| const view = viewRef.current | ||
| if (!view) return '' | ||
| const { from, to } = view.state.selection.main | ||
| return from === to ? '' : view.state.sliceDoc(from, to) | ||
| }, | ||
| getValue() { | ||
| return viewRef.current?.state.doc.toString() ?? '' | ||
| }, | ||
| getRunnableSql() { | ||
| const view = viewRef.current | ||
| if (!view) return '' | ||
| return getRunnableSqlFromView(view) | ||
| }, | ||
| setValue(nextValue: string) { | ||
| const view = viewRef.current | ||
| if (!view) return | ||
| const currentDoc = view.state.doc.toString() | ||
| if (currentDoc === nextValue) return | ||
| lastEmittedRef.current = nextValue | ||
| view.dispatch({ | ||
| changes: { | ||
| from: 0, | ||
| to: currentDoc.length, | ||
| insert: nextValue, | ||
| }, | ||
| }) | ||
| }, | ||
| formatSql() { | ||
| const view = viewRef.current | ||
| if (!view) return | ||
| void (async () => { | ||
| try { | ||
| const formatted = await formatSqlDocument(view.state.doc.toString()) | ||
| view.dispatch({ | ||
| changes: { from: 0, to: view.state.doc.length, insert: formatted }, | ||
| }) | ||
| lastEmittedRef.current = formatted | ||
| onChangeRef.current(formatted) | ||
| } catch { | ||
| // Ignore formatter load/parse errors and keep the editor usable. | ||
| } | ||
| })() | ||
| }, | ||
| getView() { | ||
| return viewRef.current | ||
| }, | ||
| })) | ||
| for (const schema of schemas) { | ||
| for (const table of schema.tables) { | ||
| const qualifiedName = `${schema.name}.${table.name}` | ||
| // Look up columns from tableColumns prop | ||
| const colKey = Object.keys(tableColumns).find(k => k.endsWith(`:${schema.name}.${table.name}`)) | ||
| const cols = colKey ? tableColumns[colKey] : [] | ||
| schemaMap[qualifiedName] = cols | ||
| if (schema.name === currentSchema) { | ||
| schemaMap[table.name] = cols | ||
| } | ||
| const identifierOptions = useMemo<Completion[]>(() => { | ||
| const activeSchema = schemas.find((schema) => schema.name === currentSchema) ?? null | ||
| const options: Completion[] = [] | ||
| const seen = new Set<string>() | ||
| if (!activeSchema) { | ||
| return options | ||
| } | ||
| for (const table of activeSchema.tables) { | ||
| if (!seen.has(table.name)) { | ||
| seen.add(table.name) | ||
| options.push({ | ||
| label: table.name, | ||
| type: 'class', | ||
| detail: 'table', | ||
| }) | ||
| } | ||
| } | ||
| return { | ||
| dialect: MySQL, | ||
| schema: schemaMap, | ||
| defaultSchema: currentSchema, | ||
| for (const [cacheKey, columns] of Object.entries(tableColumns)) { | ||
| if (!cacheKey.includes(`:${activeSchema.name}.`)) continue | ||
| for (const column of columns) { | ||
| if (seen.has(column)) continue | ||
| seen.add(column) | ||
| options.push({ | ||
| label: column, | ||
| type: 'property', | ||
| detail: 'column', | ||
| }) | ||
| } | ||
| } | ||
| return options | ||
| }, [schemas, currentSchema, tableColumns]) | ||
| // Create editor | ||
| const identifierCompletionSource = useMemo(() => { | ||
| const normalizedOptions = identifierOptions.map((option) => ({ | ||
| ...option, | ||
| boost: option.detail === 'table' ? 2 : 1, | ||
| apply: option.label, | ||
| })) | ||
| return (context: CompletionContext) => { | ||
| const word = context.matchBefore(/[`\w$]*$/) | ||
| if (!word) return null | ||
| if (word.from === word.to && !context.explicit) return null | ||
| const query = word.text.replaceAll('`', '').toLowerCase() | ||
| const filtered = query | ||
| ? normalizedOptions.filter((option) => option.label.toLowerCase().includes(query)).slice(0, 80) | ||
| : normalizedOptions.slice(0, 80) | ||
| if (filtered.length === 0) { | ||
| return null | ||
| } | ||
| return { | ||
| from: word.from, | ||
| to: word.to, | ||
| options: filtered, | ||
| validFor: /[`\w$]*/, | ||
| } | ||
| } | ||
| }, [identifierOptions]) | ||
| useEffect(() => { | ||
@@ -171,2 +456,21 @@ if (!containerRef.current) return | ||
| run: () => { | ||
| const view = viewRef.current | ||
| if (!view) { | ||
| onRunRef.current?.() | ||
| return true | ||
| } | ||
| const { from, to } = view.state.selection.main | ||
| if (from !== to && onRunSelectionRef.current) { | ||
| onRunSelectionRef.current(view.state.sliceDoc(from, to)) | ||
| return true | ||
| } | ||
| onRunSelectionRef.current?.(getRunnableSqlFromView(view)) | ||
| return true | ||
| }, | ||
| }, | ||
| { | ||
| key: 'Shift-Mod-Enter', | ||
| run: () => { | ||
| onRunRef.current?.() | ||
@@ -177,3 +481,15 @@ return true | ||
| ]) | ||
| const completionAcceptKeymap = Prec.highest(keymap.of([ | ||
| { | ||
| key: 'Enter', | ||
| run: (view) => completionStatus(view.state) === 'active' ? acceptCompletion(view) : false, | ||
| }, | ||
| { | ||
| key: 'Tab', | ||
| run: (view) => completionStatus(view.state) === 'active' ? acceptCompletion(view) : false, | ||
| }, | ||
| ])) | ||
| const dark = isDark() | ||
| const startState = EditorState.create({ | ||
@@ -184,10 +500,18 @@ doc: value, | ||
| navidogTheme, | ||
| sqlCompartment.of(sql({ | ||
| dialect: MySQL, | ||
| schema: {}, | ||
| })), | ||
| autocompletion({ | ||
| highlightCompartment.of(syntaxHighlighting(dark ? darkHighlightStyle : lightHighlightStyle)), | ||
| sqlCompartment.of(sql({ dialect: MySQL })), | ||
| autocompleteCompartment.of(autocompletion({ | ||
| activateOnTyping: true, | ||
| activateOnTypingDelay: 180, | ||
| maxRenderedOptions: 30, | ||
| }), | ||
| interactionDelay: 0, | ||
| selectOnOpen: true, | ||
| defaultKeymap: true, | ||
| override: [ | ||
| identifierCompletionSource, | ||
| keywordCompletionSource(MySQL), | ||
| ], | ||
| })), | ||
| completionAcceptKeymap, | ||
| Prec.highest(keymap.of(completionKeymap)), | ||
| runKeymap, | ||
@@ -198,3 +522,5 @@ keymap.of([...defaultKeymap, indentWithTab]), | ||
| if (update.docChanged) { | ||
| onChangeRef.current(update.state.doc.toString()) | ||
| const newDoc = update.state.doc.toString() | ||
| lastEmittedRef.current = newDoc | ||
| onChangeRef.current(newDoc) | ||
| } | ||
@@ -213,3 +539,15 @@ }), | ||
| // Watch for theme changes | ||
| const observer = new MutationObserver(() => { | ||
| const nowDark = isDark() | ||
| view.dispatch({ | ||
| effects: highlightCompartment.reconfigure( | ||
| syntaxHighlighting(nowDark ? darkHighlightStyle : lightHighlightStyle) | ||
| ), | ||
| }) | ||
| }) | ||
| observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }) | ||
| return () => { | ||
| observer.disconnect() | ||
| view.destroy() | ||
@@ -219,11 +557,14 @@ viewRef.current = null | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, []) // Only create once | ||
| }, []) | ||
| // Sync external value changes (e.g., from history clicks) | ||
| useEffect(() => { | ||
| if (suppressExternalValueSync) return | ||
| const view = viewRef.current | ||
| if (!view) return | ||
| // Skip if this value came from our own onChange emission | ||
| if (value === lastEmittedRef.current) return | ||
| const currentDoc = view.state.doc.toString() | ||
| if (currentDoc !== value) { | ||
| lastEmittedRef.current = value | ||
| view.dispatch({ | ||
@@ -237,5 +578,4 @@ changes: { | ||
| } | ||
| }, [value]) | ||
| }, [value, suppressExternalValueSync]) | ||
| // Reconfigure SQL language when schema changes | ||
| useEffect(() => { | ||
@@ -246,5 +586,16 @@ const view = viewRef.current | ||
| view.dispatch({ | ||
| effects: sqlCompartment.reconfigure(sql(sqlConfig)), | ||
| effects: autocompleteCompartment.reconfigure(autocompletion({ | ||
| activateOnTyping: true, | ||
| activateOnTypingDelay: 180, | ||
| maxRenderedOptions: 30, | ||
| interactionDelay: 0, | ||
| selectOnOpen: true, | ||
| defaultKeymap: true, | ||
| override: [ | ||
| identifierCompletionSource, | ||
| keywordCompletionSource(MySQL), | ||
| ], | ||
| })), | ||
| }) | ||
| }, [sqlConfig]) | ||
| }, [identifierCompletionSource]) | ||
@@ -257,2 +608,4 @@ return ( | ||
| ) | ||
| } | ||
| }) | ||
| export default SqlEditor |
+30
-0
@@ -49,2 +49,22 @@ /* ── Connection ──────────────────────────────── */ | ||
| export type TableIndex = { | ||
| name: string | ||
| unique: boolean | ||
| primary: boolean | ||
| type: string | ||
| columns: string[] | ||
| cardinality?: number | null | ||
| comment?: string | ||
| } | ||
| export type TableForeignKey = { | ||
| name: string | ||
| columns: string[] | ||
| referencedSchema: string | ||
| referencedTable: string | ||
| referencedColumns: string[] | ||
| onUpdate: string | ||
| onDelete: string | ||
| } | ||
| export type ImportCreateTablePlan = { | ||
@@ -100,2 +120,7 @@ columns: TableColumn[] | ||
| export type TableMetadataResponse = { | ||
| indexes: TableIndex[] | ||
| foreignKeys: TableForeignKey[] | ||
| } | ||
| export type QueryExecutionResponse = { | ||
@@ -127,3 +152,7 @@ durationMs: number | ||
| page: number | ||
| pageCursors: (Record<string, unknown> | null)[] | ||
| pagingMode: 'offset' | 'primaryKey' | ||
| totalRows: number | ||
| hasMore: boolean | ||
| rowCountExact: boolean | ||
| loading: boolean | ||
@@ -142,2 +171,3 @@ } | ||
| durationMs: number | ||
| activeQueryId?: string | ||
| loading: boolean | ||
@@ -144,0 +174,0 @@ } |
| :root{--bg-app: #ececec;--bg-toolbar: #e8e8e8;--bg-sidebar: #f5f5f5;--bg-main: #ffffff;--bg-info: #fafafa;--bg-statusbar: #e8e8e8;--bg-modal-overlay: rgba(0, 0, 0, .35);--border: #d4d4d4;--border-light: #e6e6e6;--border-dark: #bbb;--select-bg: #2968c8;--select-bg-hover: #e8f0fe;--select-text: #ffffff;--text: #333333;--text-secondary: #777777;--text-muted: #aaaaaa;--accent: #2968c8;--accent-hover: #1a54b0;--danger: #d93025;--success: #1e8e3e;--warning: #f9a825;--table-header-bg: #f0f0f0;--table-row-alt: #f9f9fc;--table-row-hover: #eef3fb;--table-border: #e2e2e2;--tab-active-bg: #ffffff;--tab-inactive-bg: #e8e8e8;--tab-border: #cccccc;--toolbar-h: 72px;--statusbar-h: 26px;--sidebar-w: 240px;--info-w: 360px;--tab-h: 32px;--font-sans: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", "Segoe UI", sans-serif;--font-mono: "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", monospace;--font-size: 13px;--font-size-sm: 13px;--font-size-xs: 12px}[data-theme=dark]{--bg-app: #1e1e1e;--bg-toolbar: #252526;--bg-sidebar: #1e1e1e;--bg-main: #1e1e1e;--bg-info: #252526;--bg-statusbar: #1e1e1e;--bg-modal-overlay: rgba(0, 0, 0, .6);--border: #3c3c3c;--border-light: #2d2d2d;--border-dark: #4a4a4a;--select-bg: #264f78;--select-bg-hover: #2a2d2e;--select-text: #ffffff;--text: #cccccc;--text-secondary: #969696;--text-muted: #5a5a5a;--accent: #0078d4;--accent-hover: #1a8aed;--danger: #f44747;--success: #4ec9b0;--warning: #cca700;--table-header-bg: #2d2d2d;--table-row-alt: #232323;--table-row-hover: #2a2d2e;--table-border: #3c3c3c;--tab-active-bg: #1e1e1e;--tab-inactive-bg: #2d2d2d;--tab-border: #3c3c3c;color-scheme:dark}[data-theme=dark] input,[data-theme=dark] select,[data-theme=dark] textarea{background:#2d2d2d;color:#ccc;border-color:#3c3c3c}[data-theme=dark] .history-panel,[data-theme=dark] .combo-box-dropdown{background:#252526}[data-theme=dark] .cli-container{background:#1b1b1b}[data-theme=dark] .cli-output{color:#ccc}[data-theme=dark] .cli-welcome{color:#5a5a5a;border-bottom-color:#3c3c3c}[data-theme=dark] .cli-result{color:#969696}[data-theme=dark] .import-error-list{background:#3a2020;border-color:#5a3030}[data-theme=dark] .toolbar-btn:hover{background:#ffffff14}[data-theme=dark] .toolbar-btn:active{background:#ffffff1f}[data-theme=dark] .toolbar-btn.active{background:#264f78;color:#6cb6ff}*,*:before,*:after{box-sizing:border-box}body{margin:0;font-family:var(--font-sans);font-size:var(--font-size);color:var(--text);background:var(--bg-app);overflow:hidden}button,input,textarea,select{font:inherit}button{cursor:pointer;border:none;background:none}ul,ol{list-style:none;margin:0;padding:0}#root{height:100vh}.app-shell{display:flex;flex-direction:column;height:100vh;overflow:hidden}.toolbar{height:var(--toolbar-h);background:var(--bg-toolbar);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 12px;gap:2px;flex-shrink:0;-webkit-user-select:none;user-select:none}.toolbar-btn{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;padding:6px 12px;border-radius:6px;color:var(--text);font-size:var(--font-size-xs);min-width:56px;transition:background .12s}.toolbar-btn:hover{background:#0000000f}.toolbar-btn:active{background:#0000001a}.toolbar-btn.active{background:#e8f0fe;color:#1a73e8}.toolbar-btn .tb-icon{font-size:24px;line-height:1;display:flex;align-items:center;justify-content:center;width:32px;height:32px}.toolbar-btn .tb-label{white-space:nowrap}.toolbar-sep{width:1px;height:44px;background:var(--border);margin:0 6px;flex-shrink:0}.toolbar-spacer{flex:1}.workspace{flex:1;display:flex;overflow:hidden;min-height:0}.sidebar{width:var(--sidebar-w);min-width:140px;background:var(--bg-sidebar);border-right:none;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}.panel-resize-handle.vertical{width:5px;cursor:col-resize;background:var(--border);flex-shrink:0;position:relative;transition:background .15s;z-index:5;align-self:stretch}.panel-resize-handle.vertical:before{content:"";position:absolute;inset:0 -3px;z-index:6}.panel-resize-handle.vertical:hover,.panel-resize-handle.vertical:active{background:var(--accent)}.panel-resize-handle.vertical:after{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);height:30px;width:2px;border-left:1px solid rgba(0,0,0,.15);border-right:1px solid rgba(0,0,0,.15)}.sidebar-header{padding:8px 12px 6px;font-size:var(--font-size-sm);font-weight:600;color:var(--text-secondary);border-bottom:1px solid var(--border-light);display:flex;align-items:center;gap:6px;flex-shrink:0}.sidebar-search{margin:6px 8px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-sm);background:#fff;outline:none;flex-shrink:0}.sidebar-search:focus{border-color:var(--accent)}.sidebar-search::placeholder{color:var(--text-muted)}.tree-container{flex:1;overflow-y:auto;overflow-x:hidden;padding:2px 0}.tree-node{display:flex;align-items:center;padding:2px 8px 2px 0;cursor:pointer;font-size:var(--font-size-sm);white-space:nowrap;-webkit-user-select:none;user-select:none;border:1px solid transparent;min-height:24px}.tree-node:hover{background:var(--select-bg-hover)}.tree-node.selected{background:var(--select-bg);color:var(--select-text)}.tree-node.selected:hover{background:var(--accent-hover)}.tree-arrow{width:16px;height:16px;display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--text-secondary);flex-shrink:0;transition:transform .12s ease}.tree-arrow.expanded{transform:rotate(90deg)}.tree-node.selected .tree-arrow{color:var(--select-text)}.conn-color-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.tree-icon{width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:4px;font-size:14px}.tree-label{overflow:hidden;text-overflow:ellipsis}.main-area{flex:1;display:flex;flex-direction:column;min-width:0;background:var(--bg-main);overflow:hidden}.tab-bar{display:flex;align-items:flex-end;background:var(--tab-inactive-bg);border-bottom:1px solid var(--border);flex-shrink:0;min-height:var(--tab-h);overflow-x:auto;overflow-y:hidden;-webkit-user-select:none;user-select:none}.tab-bar::-webkit-scrollbar{height:0}.tab-item{display:flex;align-items:center;gap:6px;padding:6px 12px;font-size:var(--font-size-sm);border:1px solid var(--tab-border);border-bottom:none;border-radius:4px 4px 0 0;background:var(--tab-inactive-bg);color:var(--text-secondary);margin-left:-1px;cursor:pointer;white-space:nowrap;max-width:200px;position:relative;transition:background .1s}.tab-item:first-child{margin-left:4px}.tab-item:hover{background:#f0f0f0}.tab-item.active{background:var(--tab-active-bg);color:var(--text);border-bottom:1px solid var(--tab-active-bg);z-index:1;font-weight:500}.tab-item .tab-icon{font-size:14px}.tab-item .tab-title{overflow:hidden;text-overflow:ellipsis}.tab-close{width:16px;height:16px;display:flex;align-items:center;justify-content:center;border-radius:3px;font-size:14px;color:var(--text-muted);flex-shrink:0;line-height:1}.tab-close:hover{background:#0000001a;color:var(--text)}.tab-item.active .tab-close{color:var(--text-secondary)}.tab-content{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0}.empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;color:var(--text-muted);font-size:14px}.empty-state .empty-icon{font-size:48px;opacity:.4}.empty-state .empty-title{font-size:16px;color:var(--text-secondary);font-weight:500}.objects-toolbar{display:flex;align-items:center;padding:6px 8px;gap:6px;border-bottom:1px solid var(--border-light);flex-shrink:0}.objects-toolbar button{padding:3px 6px;border-radius:3px;font-size:var(--font-size-sm);color:var(--text-secondary)}.objects-toolbar button:hover{background:#0000000f}.objects-search{padding:3px 8px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-sm);width:180px;background:#fff;outline:none;margin-left:auto}.objects-search:focus{border-color:var(--accent)}.objects-view-toggle{display:flex;gap:2px}.view-btn{padding:4px 6px!important;font-size:14px!important}.view-btn.active{background:#00000014!important}.objects-grid{flex:1;overflow:auto}.objects-table{width:100%;border-collapse:collapse}.objects-table th{position:sticky;top:0;background:var(--table-header-bg);border-bottom:1px solid var(--table-border);padding:5px 12px;text-align:left;font-size:var(--font-size-sm);font-weight:500;color:var(--text-secondary);white-space:nowrap;z-index:1}.objects-table td{padding:4px 12px;border-bottom:1px solid var(--border-light);font-size:var(--font-size-sm);white-space:nowrap}.objects-table tr:hover td{background:var(--table-row-hover)}.objects-table .obj-icon{margin-right:6px;font-size:14px}.data-toolbar{display:flex;align-items:center;padding:4px 8px;gap:4px;border-bottom:1px solid var(--border-light);flex-shrink:0;background:var(--bg-info)}.data-toolbar button{padding:3px 8px;border-radius:3px;font-size:var(--font-size-sm);color:var(--text-secondary)}.data-toolbar button:hover{background:#0000000f}.filter-label{font-size:var(--font-size-xs);color:var(--text-secondary);white-space:nowrap}.filter-input{flex:1;max-width:300px;min-width:120px;padding:2px 8px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-xs);font-family:var(--font-mono);outline:none;transition:border-color .15s}.filter-input:focus{border-color:var(--accent)}.filter-clear{background:none;border:none;color:var(--danger);cursor:pointer;font-size:12px;padding:2px 4px;border-radius:3px}.filter-clear:hover{background:#fee}.sort-indicator{font-size:11px;color:var(--accent);font-weight:700}.col-header{position:relative;cursor:default}.col-header-label{flex:1}.col-sort-trigger{opacity:0;font-size:10px;color:var(--fg-dim);cursor:pointer;padding:0 2px;transition:opacity .15s;-webkit-user-select:none;user-select:none}.col-header:hover .col-sort-trigger{opacity:1}.col-sort-menu{position:absolute;top:100%;left:0;z-index:200;background:#fff;border:1px solid var(--border);border-radius:6px;box-shadow:0 4px 16px #00000038;min-width:140px;padding:4px 0;font-size:13px;font-weight:400;white-space:nowrap}.col-sort-item{padding:6px 12px;cursor:pointer;display:flex;align-items:center;gap:2px}.col-sort-item:hover{background:var(--hover)}.col-sort-sep{height:1px;background:var(--border);margin:4px 0}.filter-tag{display:inline-flex;align-items:center;gap:4px;background:#e8f0fe;color:#1a73e8;font-size:12px;padding:2px 8px;border-radius:12px;margin-right:4px;white-space:nowrap}.filter-tag strong{font-weight:600}.filter-tag-close{background:none;border:none;color:#1a73e8;cursor:pointer;font-size:11px;padding:0 2px;line-height:1}.filter-tag-close:hover{color:#c62828}.filter-clear-all{background:none;border:1px solid var(--border);border-radius:4px;color:var(--fg-dim);font-size:11px;cursor:pointer;padding:2px 8px;margin-left:4px}.filter-clear-all:hover{background:var(--hover)}.toolbar-icon-btn{background:none;border:1px solid transparent;border-radius:4px;font-size:16px;cursor:pointer;padding:2px 6px}.toolbar-icon-btn:hover{background:var(--hover)}.toolbar-icon-btn.active{background:#e8f0fe;border-color:#4285f4}.navi-filter-panel{border-bottom:1px solid #d8d8d8;background:#fafafa}.navi-filter-conditions{padding:10px 14px;min-height:40px}.navi-filter-row{display:flex;align-items:center;gap:6px;padding:3px 0;font-size:13px}.navi-filter-check{width:16px;height:16px;margin:0;cursor:pointer;accent-color:#4285f4}.navi-filter-col-select{color:#06c;font-weight:600;font-size:13px;border:none;background:transparent;cursor:pointer;outline:none;padding:2px 4px;appearance:none;-webkit-appearance:none}.navi-filter-col-select:hover{text-decoration:underline}.navi-filter-op{color:#333;padding:1px 2px;border:1px solid #d0d0d0;border-radius:3px;background:#fff;font-size:12px;cursor:pointer;min-width:50px;outline:none}.navi-filter-val{color:#333;cursor:pointer;padding:2px 8px;background:#fff;border:1px solid #d0d0d0;border-radius:3px;min-width:20px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.navi-filter-val:hover{border-color:#999}.navi-filter-connector{color:#999;font-size:12px;padding:0 4px}.navi-filter-btn-icon{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;font-size:14px;font-weight:700;color:#fff;background:#4285f4;border-radius:3px;cursor:pointer;margin-left:2px}.navi-filter-btn-icon:hover{background:#3367d6}.navi-filter-btn-group{background:#7baaf7;font-size:11px}.navi-filter-btn-group:hover{background:#5a94e6}.navi-filter-remove{background:none;border:none;color:#ccc;cursor:pointer;font-size:12px;padding:0 4px;margin-left:auto}.navi-filter-remove:hover{color:#c62828}.navi-filter-group{margin:2px 0}.navi-filter-group-paren{font-size:15px;font-weight:600;color:#555;padding:0 2px}.navi-filter-group-children{padding-left:24px}.navi-filter-toolbar{display:flex;align-items:center;gap:2px;padding:4px 14px;background:#f0f0f0;border-top:1px solid #e0e0e0}.navi-filter-tool{width:26px;height:24px;display:flex;align-items:center;justify-content:center;font-size:14px;color:#555;cursor:pointer;border-radius:4px}.navi-filter-tool:hover{background:#d6d6d6}.navi-filter-apply{font-size:13px;color:#333;cursor:pointer;padding:2px 10px;border-radius:4px;font-weight:500}.navi-filter-apply:hover{background:#d6d6d6}.navi-filter-status{font-size:12px;color:#999;margin-left:8px}.navi-picker-overlay{position:absolute;inset:0;z-index:300}.navi-picker{position:absolute;top:60px;right:40px;width:380px;max-height:440px;background:#f6f6f6;border-radius:10px;box-shadow:0 8px 32px #00000040;display:flex;flex-direction:column;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.navi-picker-input{margin:12px 14px 0;padding:7px 8px;border:2px solid #4a90d9;border-radius:4px;font-size:13px;outline:none;background:#fff}.navi-picker-input:focus{border-color:#2563eb}.navi-picker-section{padding:10px 14px 4px;font-size:13px;color:#666;font-weight:500}.navi-picker-list{flex:1;overflow-y:auto;min-height:60px;max-height:220px;margin:4px 10px;background:#fff;border:1px solid #d4d4d4;border-radius:6px}.navi-picker-loading{text-align:center;padding:20px;color:#999;font-size:13px}.navi-picker-item{display:flex;align-items:center;gap:8px;padding:5px 10px;cursor:pointer;font-size:13px;border-bottom:1px solid #f0f0f0;color:#333}.navi-picker-item:last-child{border-bottom:none}.navi-picker-item:hover{background:#f0f5ff}.navi-picker-item input[type=checkbox]{width:15px;height:15px;cursor:pointer;flex-shrink:0;margin:0}.navi-picker-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.navi-picker-search-row{display:flex;align-items:center;margin:6px 14px;border:1px solid #d4d4d4;border-radius:6px;background:#fff;overflow:hidden}.navi-picker-search-icon{padding:5px 6px 5px 10px;font-size:14px;color:#999;flex-shrink:0}.navi-picker-search{flex:1;border:none;outline:none;padding:6px 8px 6px 2px;font-size:13px;background:transparent}.navi-picker-actions{display:flex;justify-content:flex-end;gap:8px;padding:8px 14px 12px}.navi-picker-btn{padding:5px 22px;border:1px solid #c0c0c0;border-radius:6px;background:#fff;font-size:13px;cursor:pointer;color:#333}.navi-picker-btn:hover{background:#eaeaea}.navi-picker-btn-ok{background:#4285f4;color:#fff;border-color:#4285f4;font-weight:500}.navi-picker-btn-ok:hover{background:#3367d6}.data-grid-wrap{flex:1;overflow:auto;min-height:0}.data-grid{width:auto;border-collapse:collapse;table-layout:auto}.data-grid th{top:0;background:var(--table-header-bg);border-bottom:2px solid var(--table-border);border-right:1px solid var(--table-border);padding:4px 10px;text-align:left;font-size:var(--font-size-sm);font-weight:600;white-space:nowrap;z-index:1;min-width:60px;position:sticky;overflow:visible}.col-resize-handle{position:absolute;right:-2px;top:0;bottom:0;width:5px;cursor:col-resize;z-index:3;-webkit-user-select:none;user-select:none}.col-resize-handle:hover,.col-resize-handle:active{background:var(--accent)}.data-grid td{padding:4px 10px;border-bottom:1px solid var(--border-light);border-right:1px solid var(--border-light);font-size:var(--font-size-xs);white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis}.data-grid tr:nth-child(2n) td{background:var(--table-row-alt)}.data-grid tr:hover td{background:var(--table-row-hover)}.data-grid tr.row-selected td{background:#cce5ff!important}.data-grid tr.row-selected:hover td{background:#b8daff!important}.data-grid tr.row-selected .row-num{background:#b8daff!important;color:#004085}.data-grid tr{cursor:default}.data-grid .row-num{color:var(--text-muted);text-align:right;background:var(--table-header-bg);border-right:2px solid var(--table-border);font-family:var(--font-mono);font-size:var(--font-size-xs);min-width:40px;padding:3px 6px;position:sticky;left:0;z-index:0}.data-grid th.row-num-header{min-width:40px;z-index:2;position:sticky;left:0}.cell-null{color:var(--text-muted);font-style:italic}.data-footer{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;border-top:1px solid var(--border-light);background:var(--bg-info);font-size:var(--font-size-xs);color:var(--text-secondary);flex-shrink:0;gap:8px;flex-wrap:wrap}.data-footer-right{display:flex;align-items:center;gap:12px}.data-footer-sql{font-family:var(--font-mono);color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50%}.cell-edit-input{width:100%;min-width:60px;padding:1px 4px;border:2px solid var(--accent);border-radius:2px;font-family:var(--font-mono);font-size:var(--font-size-xs);outline:none;background:#fff;box-sizing:border-box}.cell-modified{background:#fff3cd!important}.data-grid tr.row-new td{background:#d4edda!important}.data-grid tr.row-new .row-num{background:#c3e6cb!important;color:#155724}.data-grid tr.row-deleted td{background:#f8d7da!important;text-decoration:line-through;color:#999}.data-grid tr.row-deleted .row-num{background:#f5c6cb!important;color:#721c24}.data-action-bar{display:flex;align-items:center;gap:2px;flex-shrink:0}.action-btn{width:26px;height:22px;border:1px solid var(--border);border-radius:3px;background:#fff;cursor:pointer;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);transition:background .1s,color .1s}.action-btn:hover:not(:disabled){background:var(--table-row-hover);color:var(--text)}.action-btn:disabled{opacity:.35;cursor:default}.action-btn.apply{color:#28a745;font-weight:700}.action-btn.apply:hover:not(:disabled){background:#d4edda}.action-btn.cancel{color:#dc3545;font-weight:700}.action-btn.cancel:hover:not(:disabled){background:#f8d7da}.action-status{font-size:var(--font-size-xs);color:var(--text-secondary);margin-left:8px}.pagination{display:flex;align-items:center;gap:2px}.page-btn{padding:2px 8px!important;border:1px solid var(--border)!important;border-radius:3px!important;font-size:11px!important;background:#fff!important;cursor:pointer!important;line-height:1.4!important;min-width:24px!important;text-align:center!important}.page-btn:hover{background:var(--select-bg-hover)!important}.page-btn:disabled{opacity:.35;cursor:default!important;background:#f5f5f5!important}.page-info{padding:0 6px;font-size:11px;color:var(--text-secondary);white-space:nowrap}.page-input{width:38px;padding:1px 4px;margin:0 4px;border:1px solid var(--border);border-radius:3px;font-size:11px;font-weight:600;color:var(--accent);text-align:center;background:#fff;outline:none;line-height:1.4}.page-input:focus{border-color:var(--accent);box-shadow:0 0 0 2px #2968c826}.page-count{font-size:11px;color:var(--text-secondary);white-space:nowrap;padding-left:4px;border-left:1px solid var(--border-light)}.query-pane{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}.query-toolbar{display:flex;align-items:center;padding:4px 8px;gap:8px;border-bottom:1px solid var(--border-light);flex-shrink:0;background:var(--bg-info)}.query-toolbar select{padding:2px 6px;border:1px solid var(--border);border-radius:3px;font-size:var(--font-size-sm);background:#fff}.query-db-selector{display:flex;align-items:center;gap:4px;margin-left:4px}.query-db-selector .db-selector-icon{font-size:14px}.query-db-selector select{padding:3px 24px 3px 6px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-sm);background:#fff;min-width:160px;cursor:pointer;appearance:auto;color:var(--text)}.query-db-selector select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px #2968c826}.run-btn{padding:3px 12px!important;background:var(--accent)!important;color:#fff!important;border-radius:4px!important;font-weight:500!important;font-size:var(--font-size-sm)!important}.run-btn:hover{background:var(--accent-hover)!important}.run-btn:disabled{opacity:.5}.query-split-container{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}.query-editor-area{min-height:60px;height:200px;flex-shrink:0;position:relative;overflow:hidden}.query-split-handle{height:4px;background:var(--border-light);cursor:row-resize;flex-shrink:0;position:relative;transition:background .15s}.query-split-handle:hover,.query-split-handle:active{background:var(--accent)}.query-split-handle:after{content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:30px;height:2px;border-top:1px solid rgba(0,0,0,.15);border-bottom:1px solid rgba(0,0,0,.15)}.query-results{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}.result-view-tabs{display:flex;align-items:stretch;background:var(--bg-info);border-bottom:1px solid var(--border-light);flex-shrink:0;gap:0}.result-view-tab{padding:6px 16px;font-size:var(--font-size-xs);color:var(--text-secondary);border:1px solid transparent;border-bottom:none;border-radius:4px 4px 0 0;cursor:pointer;background:none;margin-bottom:-1px;transition:all .12s}.result-view-tab:hover{color:var(--text);background:#00000008}.result-view-tab.active{color:var(--accent);background:#fff;border-color:var(--border-light);font-weight:500}.result-grid-wrap{flex:1;overflow:auto;min-height:0}.query-messages{padding:12px 16px;font-family:var(--font-mono);font-size:var(--font-size-xs);line-height:1.7;color:var(--text);overflow:auto}.query-message-item{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--border-light)}.query-message-item:last-child{border-bottom:none;margin-bottom:0}.qm-sql{color:var(--text);word-break:break-all}.qm-info{color:#666}.qm-time{color:#999}.info-panel{width:clamp(240px,20vw,500px);background:var(--bg-info);border-left:none;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden}.info-header{padding:16px 16px 12px;border-bottom:1px solid var(--border-light);display:flex;align-items:center;gap:12px;flex-shrink:0}.info-icon{width:48px;height:48px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:28px;flex-shrink:0}.info-icon.conn-icon{background:linear-gradient(135deg,#e8d5b7,#c9a96e)}.info-icon.db-icon{background:linear-gradient(135deg,#b8e6c8,#5cb87a)}.info-icon.table-icon{background:linear-gradient(135deg,#b8d4f0,#5a9fd4)}.info-title{font-size:16px;font-weight:600;line-height:1.2}.info-subtitle{font-size:var(--font-size-xs);color:var(--text-muted);margin-top:2px}.info-body{flex:1;overflow-y:auto;padding:12px 16px}.info-row{display:flex;flex-direction:column;gap:2px;margin-bottom:10px}.info-label{font-size:var(--font-size-xs);color:var(--text-muted);font-weight:500}.info-value{font-size:var(--font-size-sm);color:var(--text);word-break:break-all}.info-tab-bar{display:flex;padding:0 8px;border-bottom:1px solid var(--border-light);flex-shrink:0;background:var(--bg-info);gap:0}.info-tab{padding:6px 12px;font-size:var(--font-size-xs);color:var(--text-secondary);border:none;border-bottom:2px solid transparent;background:none;cursor:pointer;border-radius:0;font-weight:500;transition:color .12s,border-color .12s}.info-tab:hover{color:var(--text)}.info-tab.active{color:var(--accent);border-bottom-color:var(--accent)}.ddl-area{margin-top:0;padding:12px;background:#f8f8f8;border:none;border-radius:0;font-family:var(--font-mono);font-size:var(--font-size-xs);line-height:1.6;white-space:pre;overflow:auto;color:var(--text);-webkit-user-select:text;user-select:text}.sql-kw{color:#1a0dab}.sql-type{color:#0b7285}.sql-str{color:#e65100}.sql-num{color:#6a1b9a}.sql-comment{color:#6a9955;font-style:italic}.sql-punct{color:#777}.ddl-body{padding:0!important}.ddl-body .ddl-area{flex:1;max-height:none;height:100%}[data-theme=dark] .ddl-area{background:#1e1e1e}[data-theme=dark] .sql-kw{color:#569cd6}[data-theme=dark] .sql-type{color:#4ec9b0}[data-theme=dark] .sql-str{color:#ce9178}[data-theme=dark] .sql-num{color:#b5cea8}[data-theme=dark] .sql-comment{color:#6a9955}[data-theme=dark] .sql-punct{color:gray}.columns-body{padding:0!important;overflow:auto}.info-columns-table{width:100%;border-collapse:collapse;font-size:var(--font-size-xs)}.info-columns-table th{position:sticky;top:0;background:var(--table-header-bg);border-bottom:1px solid var(--table-border);padding:4px 8px;text-align:left;font-weight:600;color:var(--text-secondary);white-space:nowrap;z-index:1}.info-columns-table td{padding:3px 8px;border-bottom:1px solid var(--border-light);white-space:nowrap}.info-columns-table tr:hover td{background:var(--table-row-hover)}.info-columns-table .col-name{font-weight:500;font-family:var(--font-mono);color:var(--text)}.info-columns-table .col-type{color:var(--accent);font-family:var(--font-mono)}.info-columns-table .col-key{color:var(--warning);font-weight:600}.status-bar{height:var(--statusbar-h);background:var(--bg-statusbar);border-top:1px solid var(--border);display:flex;align-items:center;padding:0 12px;gap:16px;font-size:var(--font-size-xs);color:var(--text-secondary);flex-shrink:0;-webkit-user-select:none;user-select:none}.status-dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:4px}.status-dot.online{background:var(--success)}.status-dot.offline{background:var(--text-muted)}.modal-overlay{position:fixed;inset:0;background:var(--bg-modal-overlay);display:flex;align-items:center;justify-content:center;z-index:1000;animation:fade-in .15s ease-out}.modal-card{width:560px;max-height:80vh;background:#fff;border-radius:8px;box-shadow:0 12px 40px #00000040;display:flex;flex-direction:column;animation:slide-up .2s ease-out}.modal-title-bar{padding:14px 20px 12px;border-bottom:1px solid var(--border-light);display:flex;align-items:center;justify-content:space-between}.modal-title-bar h3{margin:0;font-size:14px;font-weight:600}.modal-close{width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;font-size:18px;color:var(--text-secondary)}.modal-close:hover{background:#0000000f}.modal-tabs{display:flex;padding:0 20px;border-bottom:1px solid var(--border-light)}.modal-tab{padding:8px 14px;font-size:var(--font-size-sm);color:var(--text-secondary);border-bottom:2px solid transparent}.modal-tab:hover{color:var(--text)}.modal-tab.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:500}.modal-body{padding:20px;flex:1;overflow-y:auto}.modal-row{display:grid;grid-template-columns:80px 1fr;align-items:center;gap:8px;margin-bottom:12px}.modal-row label{font-size:var(--font-size-sm);color:var(--text-secondary);text-align:right}.modal-row input{padding:6px 10px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-sm);outline:none;background:#fff}.modal-row input:focus{border-color:var(--accent)}.modal-row-split{display:grid;grid-template-columns:80px 1fr 60px 1fr;align-items:center;gap:8px;margin-bottom:12px}.modal-row-split label{font-size:var(--font-size-sm);color:var(--text-secondary);text-align:right}.modal-row-split input{padding:6px 10px;border:1px solid var(--border);border-radius:4px;font-size:var(--font-size-sm);outline:none;background:#fff}.modal-row-split input:focus{border-color:var(--accent)}.modal-diagram{display:flex;align-items:center;justify-content:center;gap:20px;padding:12px 0 20px}.modal-diagram-node{display:flex;flex-direction:column;align-items:center;gap:4px;font-size:var(--font-size-sm);color:var(--text-secondary)}.modal-diagram-icon{font-size:32px}.modal-diagram-line{width:40px;height:2px;background:var(--border)}.modal-checkbox{display:flex;align-items:center;gap:8px;margin-bottom:12px;padding-left:88px}.modal-checkbox input{width:16px;height:16px}.modal-checkbox span{font-size:var(--font-size-sm);color:var(--text-secondary)}.modal-footer{padding:12px 20px;border-top:1px solid var(--border-light);display:flex;align-items:center;justify-content:space-between;gap:8px}.modal-footer-left,.modal-footer-right{display:flex;gap:8px}.btn{padding:6px 16px;border-radius:4px;font-size:var(--font-size-sm);font-weight:500;border:1px solid var(--border);background:#fff;color:var(--text);transition:background .1s}.btn:hover{background:#f0f0f0}.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}.btn-primary:hover{background:var(--accent-hover)}.btn-danger{color:var(--danger);border-color:#d930254d}.btn-danger:hover{background:#d930250f}.btn-sm{padding:3px 10px;font-size:var(--font-size-xs)}.btn:disabled{opacity:.5;cursor:default}.btn-link{color:var(--accent);font-size:var(--font-size-sm);padding:4px 8px}.btn-link:hover{text-decoration:underline}.profiles-section{margin-top:16px;border-top:1px solid var(--border-light);padding-top:12px}.profiles-section h4{margin:0 0 8px;font-size:var(--font-size-sm);font-weight:600;color:var(--text-secondary)}.profile-item{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;border-radius:4px;cursor:pointer;font-size:var(--font-size-sm)}.profile-item:hover{background:var(--select-bg-hover)}.profile-item .profile-name{font-weight:500}.profile-item .profile-host{color:var(--text-muted);font-size:var(--font-size-xs)}.profile-actions{display:flex;gap:4px}@keyframes slide-up{0%{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}.resize-handle{width:4px;cursor:col-resize;background:transparent;transition:background .2s;flex-shrink:0}.resize-handle:hover{background:var(--accent)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:#ccc;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#aaa}.context-menu-overlay{position:fixed;inset:0;z-index:800}.context-menu{position:fixed;z-index:801;min-width:200px;background:#fffffff5;-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);border:1px solid rgba(0,0,0,.12);border-radius:8px;box-shadow:0 8px 32px #0000002e,0 2px 8px #00000014;padding:4px 0;animation:ctx-pop .12s ease-out;font-size:var(--font-size-sm);-webkit-user-select:none;user-select:none}.ctx-item{display:flex;align-items:center;gap:8px;padding:6px 16px 6px 12px;cursor:pointer;color:var(--text);border-radius:4px;margin:0 4px;white-space:nowrap}.ctx-item:hover{background:var(--select-bg);color:#fff}.ctx-item.danger{color:var(--danger)}.ctx-item.danger:hover{background:var(--danger);color:#fff}.ctx-item.disabled{color:var(--text-muted);cursor:default}.ctx-item.disabled:hover{background:none;color:var(--text-muted)}.ctx-item .ctx-icon{width:18px;text-align:center;font-size:14px;flex-shrink:0}.ctx-item .ctx-label{flex:1}.ctx-item .ctx-shortcut{color:var(--text-muted);font-size:var(--font-size-xs);margin-left:16px}.ctx-item:hover .ctx-shortcut{color:#ffffffb3}.ctx-item.has-submenu{position:relative}.ctx-submenu{display:none;position:absolute;left:100%;top:-4px;min-width:200px;background:var(--bg-info);border:1px solid var(--border);border-radius:8px;padding:4px 0;box-shadow:0 6px 24px #00000026;z-index:1100}.ctx-item.has-submenu:hover>.ctx-submenu{display:block}.ctx-sep{height:1px;background:#00000014;margin:4px 8px}@keyframes ctx-pop{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}[data-theme=dark] .context-menu{background:#2d2d2df7;border-color:#4a4a4a;box-shadow:0 8px 32px #00000080}[data-theme=dark] .ctx-sep{background:#ffffff14}[data-theme=dark] .ctx-submenu{background:#2d2d2df7;border-color:#4a4a4a;box-shadow:0 8px 32px #00000080}[data-theme=dark] .context-menu-overlay{background:transparent}[data-theme=dark] .cm-editor{background:#1e1e1e!important}[data-theme=dark] .cm-editor .cm-content{caret-color:#aeafad}[data-theme=dark] .cm-editor .cm-cursor{border-left-color:#aeafad!important}[data-theme=dark] .cm-editor .cm-activeLine{background:#2a2a2a!important}[data-theme=dark] .cm-editor .cm-gutters{background:#1e1e1e!important;border-right-color:#3c3c3c!important;color:#5a5a5a!important}[data-theme=dark] .cm-editor .cm-activeLineGutter{background:#2a2a2a!important;color:#ccc!important}[data-theme=dark] .cm-editor .cm-selectionBackground{background:#264f78!important}[data-theme=dark] .cm-editor .cm-matchingBracket{background:#3a5a3a!important;outline-color:#5a8a5a!important}[data-theme=dark] .cm-editor .cm-panels{background:#252526!important;border-bottom-color:#3c3c3c!important}[data-theme=dark] .cm-editor .cm-tooltip{background:#252526!important;border-color:#4a4a4a!important}[data-theme=dark] .cm-editor .cm-tooltip-autocomplete ul>li[aria-selected]{background:#264f78!important}[data-theme=dark] .cm-editor .cm-completionDetail{color:#6a6a6a!important}.notice-toast{position:fixed;top:calc(var(--toolbar-h) + 8px);right:12px;padding:8px 16px;border-radius:6px;font-size:var(--font-size-sm);z-index:900;animation:slide-in-right .2s ease-out;max-width:400px;box-shadow:0 4px 16px #00000026}.notice-toast.success{background:#e6f4ea;color:#137333;border:1px solid #a8dab5}.notice-toast.error{background:#fce8e6;color:#c5221f;border:1px solid #f5c6cb}@keyframes slide-in-right{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.history-panel{position:absolute;top:100%;left:0;width:480px;max-height:360px;background:#fff;border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 30px #00000026;z-index:100;display:flex;flex-direction:column;overflow:hidden;margin-top:4px}.history-panel-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border-light);font-size:12px;font-weight:600}.history-panel-header button{background:none;border:none;cursor:pointer;font-size:14px;padding:2px 4px;border-radius:4px}.history-panel-header button:hover{background:#0000000f}.history-empty{padding:24px;text-align:center;font-size:12px;color:var(--text-muted)}.history-list{overflow-y:auto;flex:1}.history-item{padding:6px 12px;cursor:pointer;border-bottom:1px solid var(--border-light);transition:background 80ms}.history-item:hover{background:var(--select-bg-hover)}.history-item:last-child{border-bottom:none}.history-item code{font-family:var(--font-mono);font-size:11px;color:var(--text);white-space:pre-wrap;word-break:break-all;line-height:1.4}.cli-container{height:100%;display:flex;flex-direction:column;background:#fafbfc;cursor:text}.cli-output{flex:1;overflow-y:auto;padding:12px 16px;font-family:SF Mono,Monaco,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;color:#333}.cli-welcome{color:#888;padding-bottom:8px;border-bottom:1px solid #e0e0e0;margin-bottom:8px;font-size:12px}.cli-entry{margin-bottom:6px}.cli-prompt-line{display:flex;align-items:baseline}.cli-prompt{color:#2e7d32;font-weight:600;flex-shrink:0;-webkit-user-select:none;user-select:none}.cli-cmd{color:#1565c0}.cli-result{margin:2px 0 0;white-space:pre;font-family:inherit;font-size:12px;color:#444;overflow-x:auto}.cli-result.cli-error{color:#c62828}.cli-input-line{display:flex;align-items:center;padding-top:2px}.cli-input{flex:1;background:transparent;border:none;outline:none;color:#1565c0;font-family:inherit;font-size:13px;caret-color:#333}.cli-input::placeholder{color:#aaa}.cli-input:disabled{opacity:.5}.import-wizard-overlay{position:fixed;inset:0;background:var(--bg-modal-overlay);z-index:1000;display:flex;align-items:center;justify-content:center;animation:fade-in .15s ease-out}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.import-wizard-modal{background:var(--bg-main);border-radius:10px;box-shadow:0 20px 60px #0000004d;width:760px;max-width:90vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;animation:modal-in .2s ease-out}@keyframes modal-in{0%{opacity:0;transform:translateY(20px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}.import-wizard-header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border-light);flex-shrink:0}.import-wizard-title{font-size:15px;font-weight:600}.import-close-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:6px;font-size:14px;color:var(--text-secondary)}.import-close-btn:hover{background:#00000014}.import-step-indicator{display:flex;align-items:center;justify-content:center;gap:4px;padding:14px 20px 10px;flex-shrink:0}.import-step-dot{display:flex;flex-direction:column;align-items:center;gap:4px;flex:1;position:relative}.import-step-dot:after{content:"";position:absolute;top:12px;left:calc(50% + 14px);right:calc(-50% + 14px);height:2px;background:var(--border)}.import-step-dot:last-child:after{display:none}.import-step-dot.done:after{background:var(--accent)}.import-step-dot-circle{width:24px;height:24px;border-radius:50%;background:var(--border-light);color:var(--text-secondary);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;z-index:1;transition:all .2s}.import-step-dot.active .import-step-dot-circle{background:var(--accent);color:#fff;box-shadow:0 0 0 3px #2968c833}.import-step-dot.done .import-step-dot-circle{background:var(--success);color:#fff}.import-step-dot-label{font-size:11px;color:var(--text-muted);white-space:nowrap}.import-step-dot.active .import-step-dot-label{color:var(--accent);font-weight:500}.import-step-dot.done .import-step-dot-label{color:var(--success)}.import-wizard-body{flex:1;overflow-y:auto;padding:16px 24px;min-height:300px}.import-step-title{font-size:15px;font-weight:600;margin-bottom:4px}.import-step-desc{font-size:12px;color:var(--text-secondary);margin-bottom:14px}.import-dropzone{border:2px dashed var(--border);border-radius:10px;padding:32px 20px;text-align:center;cursor:pointer;transition:border-color .2s,background .2s;margin-bottom:14px}.import-dropzone:hover{border-color:var(--accent);background:#2968c80a}.import-dropzone-hint{display:flex;flex-direction:column;align-items:center;gap:6px;color:var(--text-secondary);font-size:13px}.import-file-info{display:flex;align-items:center;justify-content:center;gap:8px;font-size:13px}.import-file-icon{font-size:24px}.import-file-name{font-weight:500}.import-file-size{color:var(--text-muted);font-size:12px}.import-options-row{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);flex-wrap:wrap}.import-options-row select{padding:3px 6px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff}.import-options-row input[type=checkbox]{margin:0;accent-color:var(--accent)}.import-preview-table-wrap{max-height:340px;overflow:auto;border:1px solid var(--border-light);border-radius:6px}.import-preview-table{width:100%;border-collapse:collapse;font-size:12px}.import-preview-table th{position:sticky;top:0;background:var(--table-header-bg);padding:5px 10px;text-align:left;font-weight:500;color:var(--text-secondary);border-bottom:1px solid var(--table-border);white-space:nowrap;z-index:1}.import-preview-table td{padding:3px 10px;border-bottom:1px solid var(--border-light);white-space:nowrap;max-width:200px;overflow:hidden;text-overflow:ellipsis}.import-preview-table .import-row-num{color:var(--text-muted);text-align:center;width:36px;font-size:11px}.import-preview-table .cell-null{color:var(--text-muted);font-style:italic}.import-preview-hint{font-size:11px;color:var(--text-muted);text-align:center;padding:6px 0}.import-target-form{display:flex;flex-direction:column;gap:12px}.import-form-row{display:flex;align-items:center;gap:8px}.import-form-row label{width:80px;font-size:13px;font-weight:500;flex-shrink:0;text-align:right}.import-form-row select{flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:5px;font-size:13px;background:#fff}.import-loading{font-size:12px;color:var(--accent)}.import-mapping-table-wrap{max-height:360px;overflow:auto;border:1px solid var(--border-light);border-radius:6px}.import-mapping-table{width:100%;border-collapse:collapse;font-size:12px}.import-mapping-table th{position:sticky;top:0;background:var(--table-header-bg);padding:6px 10px;text-align:left;font-weight:500;color:var(--text-secondary);border-bottom:1px solid var(--table-border);z-index:1}.import-mapping-table td{padding:4px 10px;border-bottom:1px solid var(--border-light)}.import-mapping-table code{background:#0000000a;padding:1px 5px;border-radius:3px;font-family:var(--font-mono);font-size:12px}.import-mapping-table select{width:100%;padding:4px 6px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff}.import-mapping-skipped{opacity:.4}.import-preview-cell{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary);font-family:var(--font-mono);font-size:11px}.import-mapping-summary{font-size:12px;color:var(--text-secondary);text-align:right;padding:8px 0 0}.import-stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px}.import-stat{background:var(--bg-info);border:1px solid var(--border-light);border-radius:8px;padding:12px;text-align:center}.import-stat-label{font-size:11px;color:var(--text-secondary);margin-bottom:4px}.import-stat-value{font-size:20px;font-weight:600}.import-progress-bar-wrap{height:8px;background:var(--border-light);border-radius:4px;overflow:hidden;margin-bottom:6px}.import-progress-bar{height:100%;background:linear-gradient(90deg,var(--accent),#4fc3f7);border-radius:4px;transition:width .3s ease}.import-progress-text{font-size:12px;color:var(--text-secondary);text-align:center}.import-error-log{margin-top:14px}.import-error-list{max-height:120px;overflow-y:auto;border:1px solid #f5c6cb;border-radius:6px;background:#fef5f5;padding:8px 12px}.import-error-item{font-size:11px;color:#c62828;padding:2px 0;font-family:var(--font-mono)}.import-wizard-footer{display:flex;align-items:center;padding:12px 20px;border-top:1px solid var(--border-light);gap:8px;flex-shrink:0}.import-btn{padding:7px 18px;border-radius:6px;font-size:13px;font-weight:500;border:none;cursor:pointer;transition:background .12s}.import-btn:disabled{opacity:.4;cursor:not-allowed}.import-btn-primary{background:var(--accent);color:#fff}.import-btn-primary:hover:not(:disabled){background:var(--accent-hover)}.import-btn-secondary{background:var(--border-light);color:var(--text)}.import-btn-secondary:hover:not(:disabled){background:var(--border)}.combo-box-input{width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:5px;font-size:13px;background:#fff;outline:none;transition:border-color .15s}.combo-box-input:focus{border-color:var(--accent)}.combo-box-dropdown{position:absolute;top:100%;left:0;right:0;z-index:100;background:#fff;border:1px solid var(--border);border-top:none;border-radius:0 0 6px 6px;box-shadow:0 6px 20px #00000026;max-height:240px;overflow-y:auto}.combo-box-item{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;font-size:12px;cursor:pointer;transition:background 80ms}.combo-box-item:hover{background:var(--select-bg-hover)}.combo-box-item.selected{background:var(--select-bg);color:var(--select-text)}.combo-box-item.combo-box-new{color:var(--accent);border-bottom:1px solid var(--border-light)}.combo-box-meta{font-size:11px;color:var(--text-muted)}.combo-box-item.selected .combo-box-meta{color:#ffffffb3}.combo-box-empty{padding:8px 10px;font-size:12px;color:var(--text-muted);text-align:center}.export-step-title{font-size:15px;font-weight:600;margin-bottom:4px}.export-step-desc{font-size:12px;color:var(--text-secondary);margin-bottom:14px}.export-format-list{display:flex;flex-direction:column;gap:6px}.export-format-item{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--border-light);border-radius:8px;cursor:pointer;transition:border-color .15s,background .15s}.export-format-item:hover{border-color:var(--border);background:#00000004}.export-format-item.selected{border-color:var(--accent);background:#2968c80a}.export-format-item input[type=radio]{accent-color:var(--accent);margin:0}.export-format-info{display:flex;flex-direction:column;gap:2px}.export-format-label{font-size:13px;font-weight:500}.export-format-desc{font-size:11px;color:var(--text-muted)}.export-form-row{display:flex;align-items:center;gap:8px;margin-bottom:12px}.export-form-row label{width:70px;font-size:13px;font-weight:500;flex-shrink:0;text-align:right}.export-form-row select{flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:5px;font-size:13px;background:#fff}.export-table-search-row{display:flex;align-items:center;gap:4px;margin-bottom:6px}.export-table-search{flex:1;padding:5px 10px;border:1px solid var(--border);border-radius:5px;font-size:12px;background:#fff;outline:none}.export-table-search:focus{border-color:var(--accent)}.export-table-search-clear{padding:2px 6px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff;cursor:pointer;color:var(--text-muted)}.export-table-search-clear:hover{color:var(--danger);border-color:var(--danger)}.export-table-list-header{font-size:12px;padding:4px 0;color:var(--text-secondary);display:flex;align-items:center;gap:6px}.export-table-list-header input[type=checkbox]{accent-color:var(--accent)}.export-table-list{border:1px solid var(--border-light);border-radius:6px;max-height:280px;overflow-y:auto}.export-table-list-row{display:flex;align-items:center;gap:8px;padding:5px 10px;font-size:12px;border-bottom:1px solid var(--border-light)}.export-table-list-row:last-child{border-bottom:none}.export-table-list-head{background:var(--table-header-bg);font-weight:500;color:var(--text-secondary);position:sticky;top:0;z-index:1}.export-table-list-row.disabled{opacity:.4}.export-table-list-row input[type=checkbox]{accent-color:var(--accent);margin:0}.export-table-name{display:flex;align-items:center;gap:4px}.export-table-icon{font-size:14px}.export-filename-input{width:100%;padding:2px 6px;border:1px solid var(--border-light);border-radius:3px;font-size:12px;font-family:var(--font-mono);background:#fff}.export-filename-input:focus{border-color:var(--accent);outline:none}.export-loading{font-size:12px;color:var(--accent);padding:20px;text-align:center}.export-col-list{border:1px solid var(--border-light);border-radius:6px;max-height:300px;overflow-y:auto;padding:4px 0}.export-col-item{display:flex;align-items:center;gap:8px;padding:4px 14px;cursor:pointer;font-size:13px;transition:background .1s}.export-col-item:hover{background:var(--select-bg-hover)}.export-col-item input[type=checkbox]{accent-color:var(--accent);margin:0}.export-col-name{flex:1}.export-col-type{font-size:11px;color:var(--text-muted);font-family:var(--font-mono)}.export-col-key{font-size:12px}.export-col-actions{display:flex;align-items:center;gap:8px;padding:8px 0 0;font-size:12px}.export-col-actions button{padding:3px 10px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff;cursor:pointer}.export-col-actions button:hover{background:var(--select-bg-hover)}.export-col-actions input[type=checkbox]{accent-color:var(--accent)}.export-options-section{margin-bottom:16px}.export-options-title{font-size:13px;font-weight:600;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border-light)}.export-options-grid{display:flex;flex-direction:column;gap:8px}.export-options-grid input[type=checkbox]{accent-color:var(--accent)}.export-option-row{display:flex;align-items:center;gap:8px;font-size:13px}.export-option-row span:first-child{width:110px;text-align:right;color:var(--text-secondary);flex-shrink:0}.export-option-row select{padding:3px 8px;border:1px solid var(--border);border-radius:4px;font-size:12px;background:#fff}.export-stats-row{display:flex;gap:20px;margin-bottom:12px;font-size:13px}.export-stat-item{display:flex;gap:6px}.export-stat-label{font-weight:500;color:var(--text-secondary)}.export-log-box{border:1px solid var(--border-light);border-radius:6px;background:var(--bg-info);min-height:180px;max-height:280px;overflow-y:auto;padding:8px 12px;font-family:var(--font-mono);font-size:12px;margin-bottom:12px}.export-log-line{padding:2px 0;line-height:1.5}.td-container{display:flex;flex-direction:column;height:100%;background:var(--bg-main)}.td-topbar{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border);background:var(--bg-info);gap:12px;flex-shrink:0}.td-topbar-left{display:flex;align-items:center;gap:8px}.td-name-label{font-size:12px;color:var(--text-secondary);font-weight:500}.td-name-input{width:220px;padding:4px 8px;border:1px solid var(--border);border-radius:4px;font-size:13px;font-family:var(--font-mono);background:var(--bg-main);color:var(--text)}.td-topbar-right{display:flex;gap:6px}.td-btn{padding:4px 10px;border:1px solid var(--border);border-radius:4px;background:var(--bg-info);color:var(--text);font-size:12px;cursor:pointer;white-space:nowrap}.td-btn:hover{background:var(--select-bg-hover)}.td-btn:disabled{opacity:.4;cursor:default}.td-btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}.td-btn-primary:hover{background:var(--accent-hover)}.td-tab-bar{display:flex;border-bottom:1px solid var(--border);background:var(--bg-info);flex-shrink:0}.td-tab{padding:6px 16px;font-size:12px;font-weight:500;color:var(--text-secondary);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer}.td-tab:hover{color:var(--text)}.td-tab.active{color:var(--accent);border-bottom-color:var(--accent)}.td-field-toolbar{display:flex;align-items:center;gap:4px;padding:6px 12px;border-bottom:1px solid var(--border-light);background:var(--bg-info);flex-shrink:0}.td-toolbar-sep{width:1px;height:18px;background:var(--border);margin:0 4px}.td-grid-wrapper{flex:1;overflow:auto}.td-grid{width:100%;border-collapse:collapse;font-size:12px}.td-grid thead th{position:sticky;top:0;background:var(--table-header-bg);border-bottom:1px solid var(--border);padding:5px 6px;text-align:left;font-weight:600;color:var(--text-secondary);font-size:11px;white-space:nowrap;z-index:1}.td-grid tbody tr{border-bottom:1px solid var(--border-light);cursor:pointer}.td-grid tbody tr:hover{background:var(--table-row-hover)}.td-grid tbody tr.td-row-selected{background:var(--select-bg-hover)}.td-grid td{padding:1px 2px}.td-cell-num{text-align:center;color:var(--text-muted);font-size:10px;width:30px}.td-cell-input{width:100%;padding:3px 6px;border:1px solid transparent;border-radius:3px;font-size:12px;font-family:var(--font-mono);background:transparent;color:var(--text);outline:none}.td-cell-input:focus{border-color:var(--accent);background:var(--bg-main)}.td-cell-select{width:100%;padding:3px 4px;border:1px solid transparent;border-radius:3px;font-size:12px;font-family:var(--font-mono);background:transparent;color:var(--text);outline:none;cursor:pointer}.td-cell-select:focus{border-color:var(--accent);background:var(--bg-main)}.td-cell-center{text-align:center}.td-cell-short{width:100%}.td-sql-preview{flex:1;overflow:auto;padding:12px;background:var(--bg-main)}.td-sql-preview pre{margin:0;font-family:var(--font-mono);font-size:12px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-all} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1995664
23.54%29
20.83%17765
20.45%12
9.09%6
200%8
60%4
100%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added