♻️ refactor(frontend-interaction): 统一标签拖拽与暗色主题交互实现

- 重构Tab拖拽排序实现,统一为可配置拖拽引擎
- 规范拖拽与点击事件边界,提升交互一致性
- 统一多组件暗色透明样式策略,减少硬编码色值
- 提升Redis/表格/连接面板在透明模式下的观感一致性
- refs #144
This commit is contained in:
Syngnat
2026-03-02 16:34:09 +08:00
parent 78c5351399
commit 7f00139847
8 changed files with 265 additions and 35 deletions

View File

@@ -67,6 +67,49 @@ body[data-theme='dark'] {
在透明窗口环境下会显著加剧 GPU 负载 */
}
/* 暗色 + 透明:提升选中/焦点可读性,避免默认蓝色在半透明背景下发灰 */
body[data-theme='dark'] .ant-tree .ant-tree-node-content-wrapper.ant-tree-node-selected,
body[data-theme='dark'] .ant-tree .ant-tree-node-content-wrapper.ant-tree-node-selected:hover {
background: rgba(246, 196, 83, 0.24) !important;
color: rgba(255, 236, 179, 0.98) !important;
}
body[data-theme='dark'] .ant-checkbox-checked .ant-checkbox-inner {
background-color: #f6c453 !important;
border-color: #f6c453 !important;
}
body[data-theme='dark'] .ant-checkbox-indeterminate .ant-checkbox-inner::after {
background-color: #f6c453 !important;
}
body[data-theme='dark'] .ant-checkbox:hover .ant-checkbox-inner,
body[data-theme='dark'] .ant-checkbox-wrapper:hover .ant-checkbox-inner {
border-color: #f6c453 !important;
}
body[data-theme='dark'] .ant-radio-checked .ant-radio-inner {
border-color: #f6c453 !important;
background-color: #f6c453 !important;
}
body[data-theme='dark'] .ant-radio-wrapper:hover .ant-radio-inner,
body[data-theme='dark'] .ant-radio:hover .ant-radio-inner {
border-color: #f6c453 !important;
}
body[data-theme='dark'] .ant-switch.ant-switch-checked {
background: #d8a93b !important;
}
body[data-theme='dark'] .ant-table-tbody > tr.ant-table-row-selected > td {
background: rgba(246, 196, 83, 0.18) !important;
}
body[data-theme='dark'] .ant-table-tbody > tr.ant-table-row-selected:hover > td {
background: rgba(246, 196, 83, 0.26) !important;
}
/* 连接配置弹窗:滚动仅在弹窗 body 内部,不使用外层 wrap 滚动条 */
.connection-modal-wrap {
overflow: hidden !important;

View File

@@ -782,6 +782,7 @@ function App() {
} as any;
const showLinuxResizeHandles = isLinuxRuntime;
const resizeGuideColor = darkMode ? 'rgba(246, 196, 83, 0.55)' : 'rgba(24, 144, 255, 0.5)';
return (
<ConfigProvider
@@ -799,6 +800,20 @@ function App() {
colorFillAlter: darkMode
? `rgba(38, 38, 38, ${effectiveOpacity})`
: `rgba(250, 250, 250, ${effectiveOpacity})`,
colorPrimary: darkMode ? '#f6c453' : '#1677ff',
colorPrimaryHover: darkMode ? '#ffd666' : '#4096ff',
colorPrimaryActive: darkMode ? '#d8a93b' : '#0958d9',
colorInfo: darkMode ? '#f6c453' : '#1677ff',
colorLink: darkMode ? '#ffd666' : '#1677ff',
colorLinkHover: darkMode ? '#ffe58f' : '#4096ff',
colorLinkActive: darkMode ? '#d8a93b' : '#0958d9',
colorPrimaryBg: darkMode ? 'rgba(246, 196, 83, 0.22)' : '#e6f4ff',
colorPrimaryBgHover: darkMode ? 'rgba(246, 196, 83, 0.30)' : '#bae0ff',
colorPrimaryBorder: darkMode ? 'rgba(246, 196, 83, 0.45)' : '#91caff',
colorPrimaryBorderHover: darkMode ? 'rgba(246, 196, 83, 0.60)' : '#69b1ff',
controlItemBgActive: darkMode ? 'rgba(246, 196, 83, 0.20)' : 'rgba(22, 119, 255, 0.12)',
controlItemBgActiveHover: darkMode ? 'rgba(246, 196, 83, 0.28)' : 'rgba(22, 119, 255, 0.18)',
controlOutline: darkMode ? 'rgba(246, 196, 83, 0.50)' : 'rgba(5, 145, 255, 0.24)',
},
components: {
Layout: {
@@ -815,7 +830,10 @@ function App() {
},
Tabs: {
cardBg: 'transparent',
itemActiveColor: darkMode ? '#177ddc' : '#1890ff',
itemActiveColor: darkMode ? '#ffd666' : '#1890ff',
itemHoverColor: darkMode ? '#ffe58f' : '#40a9ff',
itemSelectedColor: darkMode ? '#ffd666' : '#1677ff',
inkBarColor: darkMode ? '#ffd666' : '#1677ff',
}
}
}}
@@ -1241,7 +1259,7 @@ function App() {
bottom: 0,
left: 0,
width: '4px',
background: 'rgba(24, 144, 255, 0.5)',
background: resizeGuideColor,
zIndex: 9999,
pointerEvents: 'none',
display: 'none'
@@ -1256,7 +1274,7 @@ function App() {
left: sidebarWidth, // Start from sidebar edge
right: 0,
height: '4px',
background: 'rgba(24, 144, 255, 0.5)',
background: resizeGuideColor,
zIndex: 9999,
pointerEvents: 'none',
display: 'none',

View File

@@ -119,6 +119,8 @@ const ConnectionModal: React.FC<{
};
const step1SidebarDividerColor = darkMode ? STEP1_SIDEBAR_DIVIDER_DARK : STEP1_SIDEBAR_DIVIDER_LIGHT;
const step1SidebarActiveBg = darkMode ? 'rgba(246, 196, 83, 0.20)' : '#e6f4ff';
const step1SidebarActiveColor = darkMode ? '#ffd666' : '#1677ff';
const tunnelSectionStyle: React.CSSProperties = {
padding: '12px',
@@ -1293,8 +1295,8 @@ const ConnectionModal: React.FC<{
cursor: 'pointer',
borderRadius: 6,
marginBottom: 4,
background: activeGroup === idx ? '#e6f4ff' : 'transparent',
color: activeGroup === idx ? '#1677ff' : undefined,
background: activeGroup === idx ? step1SidebarActiveBg : 'transparent',
color: activeGroup === idx ? step1SidebarActiveColor : undefined,
fontWeight: activeGroup === idx ? 500 : 400,
transition: 'all 0.2s',
fontSize: 13,

View File

@@ -591,6 +591,8 @@ const DataGrid: React.FC<DataGridProps> = ({
const rowModBg = darkMode ? getRowBg(22, 34, 56) : getRowBg(230, 247, 255);
const rowAddedHover = darkMode ? getRowBg(31, 61, 31) : getRowBg(217, 247, 190);
const rowModHover = darkMode ? getRowBg(29, 53, 94) : getRowBg(186, 231, 255);
const selectionAccentHex = darkMode ? '#f6c453' : '#1890ff';
const selectionAccentRgb = darkMode ? '246, 196, 83' : '24, 144, 255';
const [form] = Form.useForm();
const [modal, contextHolder] = Modal.useModal();
@@ -3224,16 +3226,16 @@ const DataGrid: React.FC<DataGridProps> = ({
.${gridId} .ant-table-thead > tr > th .ant-table-column-sorter,
.${gridId} .ant-table-thead > tr > th .ant-table-column-sorter * { cursor: pointer !important; }
.${gridId} .ant-table-tbody > tr:hover > td { background-color: ${darkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.02)'} !important; }
.${gridId} .ant-table-tbody > tr.ant-table-row-selected > td { background-color: ${darkMode ? 'rgba(24, 144, 255, 0.15)' : 'rgba(24, 144, 255, 0.08)'} !important; }
.${gridId} .ant-table-tbody > tr.ant-table-row-selected:hover > td { background-color: ${darkMode ? 'rgba(24, 144, 255, 0.25)' : 'rgba(24, 144, 255, 0.12)'} !important; }
.${gridId} .ant-table-tbody > tr.ant-table-row-selected > td { background-color: ${darkMode ? `rgba(${selectionAccentRgb}, 0.18)` : `rgba(${selectionAccentRgb}, 0.08)`} !important; }
.${gridId} .ant-table-tbody > tr.ant-table-row-selected:hover > td { background-color: ${darkMode ? `rgba(${selectionAccentRgb}, 0.28)` : `rgba(${selectionAccentRgb}, 0.12)`} !important; }
.${gridId} .row-added td { background-color: ${rowAddedBg} !important; color: ${darkMode ? '#e6fffb' : 'inherit'}; }
.${gridId} .row-modified td { background-color: ${rowModBg} !important; color: ${darkMode ? '#e6f7ff' : 'inherit'}; }
.${gridId} .ant-table-tbody > tr.row-added:hover > td { background-color: ${rowAddedHover} !important; }
.${gridId} .ant-table-tbody > tr.row-modified:hover > td { background-color: ${rowModHover} !important; }
.${gridId}.cell-edit-mode .ant-table-tbody > tr > td[data-col-name] { user-select: none; -webkit-user-select: none; cursor: crosshair; }
.${gridId}.cell-edit-mode .ant-table-tbody > tr > td[data-cell-selected="true"] {
box-shadow: inset 0 0 0 2px #1890ff;
background-image: linear-gradient(${darkMode ? 'rgba(24, 144, 255, 0.18)' : 'rgba(24, 144, 255, 0.08)'}, ${darkMode ? 'rgba(24, 144, 255, 0.18)' : 'rgba(24, 144, 255, 0.08)'});
box-shadow: inset 0 0 0 2px ${selectionAccentHex};
background-image: linear-gradient(${darkMode ? `rgba(${selectionAccentRgb}, 0.20)` : `rgba(${selectionAccentRgb}, 0.08)`}, ${darkMode ? `rgba(${selectionAccentRgb}, 0.20)` : `rgba(${selectionAccentRgb}, 0.08)`});
}
.${gridId} .ant-table-content,
.${gridId} .ant-table-body {
@@ -3263,7 +3265,7 @@ const DataGrid: React.FC<DataGridProps> = ({
bottom: 0, // Fits container height
left: 0,
width: '2px',
background: '#1890ff',
background: selectionAccentHex,
zIndex: 9999,
display: 'none',
pointerEvents: 'none',

View File

@@ -5,6 +5,7 @@ import { useStore } from '../store';
import { RedisKeyInfo, RedisValue, StreamEntry } from '../types';
import Editor from '@monaco-editor/react';
import type { DataNode } from 'antd/es/tree';
import { normalizeOpacityForPlatform } from '../utils/appearance';
const { Search } = Input;
@@ -394,8 +395,21 @@ const buildRedisKeyTree = (
};
const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
const { connections } = useStore();
const connections = useStore(state => state.connections);
const theme = useStore(state => state.theme);
const appearance = useStore(state => state.appearance);
const darkMode = theme === 'dark';
const opacity = normalizeOpacityForPlatform(appearance.opacity);
const connection = connections.find(c => c.id === connectionId);
const keyAccentColor = darkMode ? '#ffd666' : '#1677ff';
const jsonAccentColor = darkMode ? '#f6c453' : '#1890ff';
const valueToolbarBg = darkMode
? `rgba(38, 38, 38, ${opacity})`
: `rgba(245, 245, 245, ${opacity})`;
const valueToolbarBorder = darkMode
? `1px solid rgba(255, 255, 255, ${Math.max(0.12, Math.min(0.24, opacity * 0.22))})`
: `1px solid rgba(0, 0, 0, ${Math.max(0.08, Math.min(0.2, opacity * 0.12))})`;
const valueToolbarText = darkMode ? 'rgba(255, 255, 255, 0.78)' : '#666';
const [keys, setKeys] = useState<RedisKeyInfo[]>([]);
const [loading, setLoading] = useState(false);
@@ -805,7 +819,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
overflow: 'hidden',
}}
>
<KeyOutlined style={{ color: '#1677ff', flexShrink: 0 }} />
<KeyOutlined style={{ color: keyAccentColor, flexShrink: 0 }} />
<Tooltip title={rawKey}>
<span
style={{
@@ -901,13 +915,13 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{
padding: '4px 8px',
background: '#f5f5f5',
borderBottom: '1px solid #d9d9d9',
background: valueToolbarBg,
borderBottom: valueToolbarBorder,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span style={{ fontSize: 12, color: '#666' }}>
<span style={{ fontSize: 12, color: valueToolbarText }}>
{encoding && `编码: ${encoding}`}
</span>
<Radio.Group size="small" value={viewMode} onChange={(e) => setViewMode(e.target.value)}>
@@ -920,6 +934,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
<Editor
height="calc(100% - 72px)"
language={isJson ? 'json' : 'plaintext'}
theme={darkMode ? 'transparent-dark' : 'transparent-light'}
value={displayValue}
options={{
readOnly: true,
@@ -1069,7 +1084,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
return (
<Tooltip title={<pre style={{ maxHeight: 300, overflow: 'auto', margin: 0, fontSize: 12 }}>{tooltipContent}</pre>} styles={{ root: { maxWidth: 600 } }}>
<span style={{
color: record.isBinary ? '#d46b08' : (record.isJson ? '#1890ff' : undefined),
color: record.isBinary ? '#d46b08' : (record.isJson ? jsonAccentColor : undefined),
fontFamily: record.isBinary ? 'monospace' : undefined,
fontSize: record.isBinary ? 11 : undefined
}}>
@@ -1248,7 +1263,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
return (
<Tooltip title={<pre style={{ maxHeight: 300, overflow: 'auto', margin: 0, fontSize: 12 }}>{tooltipContent}</pre>} styles={{ root: { maxWidth: 600 } }}>
<span style={{
color: record.isBinary ? '#d46b08' : (record.isJson ? '#1890ff' : undefined),
color: record.isBinary ? '#d46b08' : (record.isJson ? jsonAccentColor : undefined),
fontFamily: record.isBinary ? 'monospace' : undefined,
fontSize: record.isBinary ? 11 : undefined
}}>
@@ -1403,7 +1418,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
return (
<Tooltip title={<pre style={{ maxHeight: 300, overflow: 'auto', margin: 0, fontSize: 12 }}>{tooltipContent}</pre>} styles={{ root: { maxWidth: 600 } }}>
<span style={{
color: record.isBinary ? '#d46b08' : (record.isJson ? '#1890ff' : undefined),
color: record.isBinary ? '#d46b08' : (record.isJson ? jsonAccentColor : undefined),
fontFamily: record.isBinary ? 'monospace' : undefined,
fontSize: record.isBinary ? 11 : undefined
}}>
@@ -1557,7 +1572,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
return (
<Tooltip title={<pre style={{ maxHeight: 300, overflow: 'auto', margin: 0, fontSize: 12 }}>{tooltipContent}</pre>} styles={{ root: { maxWidth: 600 } }}>
<span style={{
color: record.isBinary ? '#d46b08' : (record.isJson ? '#1890ff' : undefined),
color: record.isBinary ? '#d46b08' : (record.isJson ? jsonAccentColor : undefined),
fontFamily: record.isBinary ? 'monospace' : undefined,
fontSize: record.isBinary ? 11 : undefined
}}>
@@ -1771,7 +1786,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
return (
<Tooltip title={<pre style={{ maxHeight: 300, overflow: 'auto', margin: 0, fontSize: 12 }}>{tooltipContent}</pre>} styles={{ root: { maxWidth: 720 } }}>
<span style={{
color: record.isBinary ? '#d46b08' : (record.isJson ? '#1890ff' : undefined),
color: record.isBinary ? '#d46b08' : (record.isJson ? jsonAccentColor : undefined),
fontFamily: record.isBinary ? 'monospace' : undefined,
fontSize: record.isBinary ? 11 : undefined
}}>
@@ -1964,6 +1979,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
<Editor
height="450px"
language={tryFormatJson(editValue).isJson ? 'json' : 'plaintext'}
theme={darkMode ? 'transparent-dark' : 'transparent-light'}
value={editValue}
onChange={(value) => setEditValue(value || '')}
options={{
@@ -2028,6 +2044,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
<Editor
height="450px"
language={jsonEditConfig?.isJson ? 'json' : 'plaintext'}
theme={darkMode ? 'transparent-dark' : 'transparent-light'}
defaultValue={jsonEditConfig?.value || ''}
onChange={(value) => { jsonEditValueRef.current = value || ''; }}
onMount={(editor) => { jsonEditValueRef.current = jsonEditConfig?.value || ''; }}

View File

@@ -1,6 +1,11 @@
import React, { useMemo } from 'react';
import React, { useMemo, useRef, useState } from 'react';
import { Tabs, Dropdown } from 'antd';
import type { MenuProps } from 'antd';
import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core';
import type { DragStartEvent, DragEndEvent } from '@dnd-kit/core';
import { SortableContext, useSortable, horizontalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
import { useStore } from '../store';
import DataViewer from './DataViewer';
import QueryEditor from './QueryEditor';
@@ -29,6 +34,54 @@ const buildTabDisplayTitle = (tab: TabData, connectionName: string | undefined):
return `[${prefix}] ${tab.title}`;
};
type SortableTabLabelProps = {
tabId: string;
displayTitle: string;
menuItems: MenuProps['items'];
draggingTabId: string | null;
onSelect: (tabId: string) => void;
};
const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
tabId,
displayTitle,
menuItems,
draggingTabId,
onSelect,
}) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: tabId });
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition: transition || 'transform 180ms cubic-bezier(0.22, 1, 0.36, 1)',
opacity: isDragging ? 0.88 : 1,
cursor: isDragging ? 'grabbing' : 'grab',
display: 'inline-flex',
alignItems: 'center',
maxWidth: '100%',
touchAction: 'none',
};
const isDragBlocked = !!draggingTabId && draggingTabId !== tabId;
return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<span
ref={setNodeRef}
style={style}
className={`tab-dnd-label ${isDragging ? 'is-dragging' : ''}`}
{...attributes}
{...listeners}
onClick={() => {
if (!isDragBlocked) onSelect(tabId);
}}
onContextMenu={(e) => e.preventDefault()}
title="拖拽调整标签顺序"
>
{displayTitle}
</span>
</Dropdown>
);
};
const TabManager: React.FC = () => {
const tabs = useStore(state => state.tabs);
const connections = useStore(state => state.connections);
@@ -39,6 +92,14 @@ const TabManager: React.FC = () => {
const closeTabsToLeft = useStore(state => state.closeTabsToLeft);
const closeTabsToRight = useStore(state => state.closeTabsToRight);
const closeAllTabs = useStore(state => state.closeAllTabs);
const moveTab = useStore(state => state.moveTab);
const [draggingTabId, setDraggingTabId] = useState<string | null>(null);
const suppressClickUntilRef = useRef<number>(0);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 },
})
);
const onChange = (newActiveKey: string) => {
setActiveTab(newActiveKey);
@@ -50,6 +111,33 @@ const TabManager: React.FC = () => {
}
};
const handleTabSelect = (tabId: string) => {
if (Date.now() < suppressClickUntilRef.current) return;
setActiveTab(tabId);
};
const handleDragStart = (event: DragStartEvent) => {
const sourceId = String(event.active.id || '').trim();
setDraggingTabId(sourceId || null);
};
const handleDragEnd = (event: DragEndEvent) => {
const sourceId = String(event.active.id || '').trim();
const targetId = String(event.over?.id || '').trim();
setDraggingTabId(null);
if (!sourceId || !targetId || sourceId === targetId) {
return;
}
suppressClickUntilRef.current = Date.now() + 120;
moveTab(sourceId, targetId);
};
const handleDragCancel = () => {
setDraggingTabId(null);
};
const tabIds = useMemo(() => tabs.map((tab) => tab.id), [tabs]);
const items = useMemo(() => tabs.map((tab, index) => {
const connectionName = connections.find((conn) => conn.id === tab.connectionId)?.name;
const displayTitle = buildTabDisplayTitle(tab, connectionName);
@@ -100,14 +188,18 @@ const TabManager: React.FC = () => {
return {
label: (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<span onContextMenu={(e) => e.preventDefault()}>{displayTitle}</span>
</Dropdown>
<SortableTabLabel
tabId={tab.id}
displayTitle={displayTitle}
menuItems={menuItems}
draggingTabId={draggingTabId}
onSelect={handleTabSelect}
/>
),
key: tab.id,
children: content,
};
}), [tabs, connections, closeOtherTabs, closeTabsToLeft, closeTabsToRight, closeAllTabs]);
}), [tabs, connections, closeOtherTabs, closeTabsToLeft, closeTabsToRight, closeAllTabs, draggingTabId]);
return (
<>
@@ -158,16 +250,53 @@ const TabManager: React.FC = () => {
.main-tabs .ant-tabs-nav::before {
border-bottom: none !important;
}
.main-tabs .ant-tabs-tab {
transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1), background-color 120ms ease;
}
.main-tabs .tab-dnd-label {
user-select: none;
-webkit-user-select: none;
}
.main-tabs .tab-dnd-label.is-dragging {
cursor: grabbing !important;
}
body[data-theme='dark'] .main-tabs .ant-tabs-tab-btn:focus-visible {
outline: none !important;
border-radius: 6px;
box-shadow: 0 0 0 2px rgba(255, 214, 102, 0.72);
background: rgba(255, 214, 102, 0.16);
}
body[data-theme='light'] .main-tabs .ant-tabs-tab-btn:focus-visible {
outline: none !important;
border-radius: 6px;
box-shadow: 0 0 0 2px rgba(9, 109, 217, 0.32);
background: rgba(9, 109, 217, 0.08);
}
body[data-theme='dark'] .main-tabs .ant-tabs-tab.ant-tabs-tab-active {
background: rgba(255, 214, 102, 0.12) !important;
border-color: rgba(255, 214, 102, 0.4) !important;
}
`}</style>
<Tabs
className="main-tabs"
type="editable-card"
onChange={onChange}
activeKey={activeTabId || undefined}
onEdit={onEdit}
items={items}
hideAdd
/>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToHorizontalAxis]}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext items={tabIds} strategy={horizontalListSortingStrategy}>
<Tabs
className="main-tabs"
type="editable-card"
onChange={onChange}
activeKey={activeTabId || undefined}
onEdit={onEdit}
items={items}
hideAdd
/>
</SortableContext>
</DndContext>
</>
);
};

View File

@@ -259,6 +259,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
const connections = useStore(state => state.connections);
const theme = useStore(state => state.theme);
const darkMode = theme === 'dark';
const resizeGuideColor = darkMode ? '#f6c453' : '#1890ff';
const readOnly = !!tab.readOnly;
const [tableHeight, setTableHeight] = useState(500);
@@ -1973,7 +1974,7 @@ END;`;
bottom: 0,
left: 0,
width: '2px',
background: '#1890ff',
background: resizeGuideColor,
zIndex: 9999,
display: 'none',
pointerEvents: 'none',

View File

@@ -337,6 +337,7 @@ interface AppState {
closeTabsToRight: (id: string) => void;
closeTabsByConnection: (connectionId: string) => void;
closeTabsByDatabase: (connectionId: string, dbName: string) => void;
moveTab: (sourceId: string, targetId: string) => void;
closeAllTabs: () => void;
setActiveTab: (id: string) => void;
setActiveContext: (context: { connectionId: string; dbName: string } | null) => void;
@@ -571,6 +572,23 @@ export const useStore = create<AppState>()(
};
}),
moveTab: (sourceId, targetId) => set((state) => {
const fromId = String(sourceId || '').trim();
const toId = String(targetId || '').trim();
if (!fromId || !toId || fromId === toId) {
return state;
}
const fromIndex = state.tabs.findIndex((tab) => tab.id === fromId);
const toIndex = state.tabs.findIndex((tab) => tab.id === toId);
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) {
return state;
}
const nextTabs = [...state.tabs];
const [movingTab] = nextTabs.splice(fromIndex, 1);
nextTabs.splice(toIndex, 0, movingTab);
return { tabs: nextTabs };
}),
closeAllTabs: () => set(() => ({ tabs: [], activeTabId: null })),
setActiveTab: (id) => set({ activeTabId: id }),