🐛 fix(frontend): 修复 Redis 搜索匹配与输入交互体验

- Redis Key 搜索默认补全包含匹配并支持 ASCII 大小写不敏感
- Redis 标签页增加连接名与 host 摘要,区分同名 db 标签
- 抽取 inputAutoCap、redisSearchPattern、tabDisplay 共享工具并补充回归测试
- 覆盖连接配置、Redis 搜索、表设计、表概览和数据表筛选输入的自动纠正问题
- 在 macOS 文本输入面板关闭局部毛玻璃,修复输入法切换出现透明框
This commit is contained in:
Syngnat
2026-04-16 18:07:38 +08:00
parent d3a1c017da
commit af90936fcc
22 changed files with 541 additions and 122 deletions

View File

@@ -4,7 +4,7 @@ import { DatabaseOutlined, ConsoleSqlOutlined, FileTextOutlined, CloudServerOutl
import { getDbIcon, getDbDefaultColor, getDbIconLabel, DB_ICON_TYPES, PRESET_ICON_COLORS } from './DatabaseIcons';
import { useStore } from '../store';
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
import {
getStoredSecretPlaceholder,
normalizeConnectionSecretErrorMessage,
@@ -12,6 +12,7 @@ import {
} from '../utils/connectionModalPresentation';
import { resolveConnectionSecretDraft } from '../utils/connectionSecretDraft';
import { getCustomConnectionDsnValidationMessage } from '../utils/customConnectionDsn';
import { applyNoAutoCapAttributes, noAutoCapInputProps } from '../utils/inputAutoCap';
import { DBGetDatabases, GetDriverStatusList, MongoDiscoverMembers, TestConnection, RedisConnect, SelectDatabaseFile, SelectSSHKeyFile } from '../../wailsjs/go/app/App';
import { ConnectionConfig, MongoMemberInfo, SavedConnection } from '../types';
@@ -24,21 +25,6 @@ const CONNECTION_MODAL_WIDTH = 960;
const CONNECTION_MODAL_BODY_HEIGHT = 620;
const STEP1_SIDEBAR_DIVIDER_DARK = 'rgba(255, 255, 255, 0.16)';
const STEP1_SIDEBAR_DIVIDER_LIGHT = 'rgba(0, 0, 0, 0.08)';
const noAutoCapInputProps = {
autoCapitalize: 'none' as const,
autoCorrect: 'off' as const,
spellCheck: false,
};
const applyNoAutoCapAttributes = (element: Element) => {
if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLTextAreaElement)) {
return;
}
element.setAttribute('autocapitalize', 'none');
element.setAttribute('autocorrect', 'off');
element.setAttribute('spellcheck', 'false');
};
type ConnectionSecretKey =
| 'primaryPassword'
| 'sshPassword'
@@ -177,6 +163,7 @@ const ConnectionModal: React.FC<{
const darkMode = theme === 'dark';
const resolvedAppearance = resolveAppearanceValues(appearance);
const effectiveOpacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
const disableLocalBackdropFilter = isMacLikePlatform();
const mysqlTopology = Form.useWatch('mysqlTopology', form) || 'single';
const mongoTopology = Form.useWatch('mongoTopology', form) || 'single';
const mongoSrv = Form.useWatch('mongoSrv', form) || false;
@@ -207,7 +194,10 @@ 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 overlayTheme = useMemo(() => buildOverlayWorkbenchTheme(darkMode), [darkMode]);
const overlayTheme = useMemo(
() => buildOverlayWorkbenchTheme(darkMode, { disableBackdropFilter: disableLocalBackdropFilter }),
[darkMode, disableLocalBackdropFilter],
);
const tunnelSectionStyle: React.CSSProperties = {
padding: '12px',
@@ -3233,5 +3223,3 @@ const ConnectionModal: React.FC<{
export default ConnectionModal;

View File

@@ -50,6 +50,7 @@ import {
} from './dataGridCopyInsert';
import { calculateAutoFitColumnWidth } from './dataGridAutoWidth';
import { buildSelectedCellClipboardText } from './dataGridSelectionCopy';
import { applyNoAutoCapAttributesWithin, noAutoCapInputProps } from '../utils/inputAutoCap';
// --- Error Boundary ---
interface DataGridErrorBoundaryState {
@@ -2234,6 +2235,7 @@ const DataGrid: React.FC<DataGridProps> = ({
// Filter State
const [filterConditions, setFilterConditions] = useState<GridFilterCondition[]>([]);
const [nextFilterId, setNextFilterId] = useState(1);
const filterPanelRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const nextConditions = normalizeGridFilterConditions(appliedFilterConditions);
@@ -2242,6 +2244,30 @@ const DataGrid: React.FC<DataGridProps> = ({
setNextFilterId(Math.max(1, maxId + 1));
}, [appliedFilterConditions, normalizeGridFilterConditions]);
useEffect(() => {
if (!showFilter) {
return;
}
const root = filterPanelRef.current;
if (!root) {
return;
}
const apply = () => {
applyNoAutoCapAttributesWithin(root);
};
apply();
if (typeof MutationObserver === 'undefined') {
return;
}
const observer = new MutationObserver(() => {
apply();
});
observer.observe(root, { childList: true, subtree: true });
return () => {
observer.disconnect();
};
}, [showFilter]);
const selectedRowKeysRef = useRef(selectedRowKeys);
const displayDataRef = useRef<any[]>([]);
@@ -5135,7 +5161,7 @@ const DataGrid: React.FC<DataGridProps> = ({
</div>
{showFilter && (
<div style={{
<div ref={filterPanelRef} style={{
padding: `${filterTopPadding}px ${panelPaddingX}px ${panelPaddingY}px ${panelPaddingX}px`,
background: 'transparent',
boxSizing: 'border-box',
@@ -5184,6 +5210,7 @@ const DataGrid: React.FC<DataGridProps> = ({
{cond.op === 'CUSTOM' ? (
<Input.TextArea
{...noAutoCapInputProps}
style={{ flex: 1 }}
autoSize={{ minRows: 1, maxRows: 4 }}
value={cond.value}
@@ -5192,6 +5219,7 @@ const DataGrid: React.FC<DataGridProps> = ({
/>
) : isListOp(cond.op) ? (
<Input.TextArea
{...noAutoCapInputProps}
style={{ flex: 1 }}
autoSize={{ minRows: 1, maxRows: 4 }}
value={cond.value}
@@ -5201,12 +5229,14 @@ const DataGrid: React.FC<DataGridProps> = ({
) : isBetweenOp(cond.op) ? (
<>
<Input
{...noAutoCapInputProps}
style={{ width: 220 }}
value={cond.value}
onChange={e => updateFilter(cond.id, 'value', e.target.value)}
placeholder="开始值"
/>
<Input
{...noAutoCapInputProps}
style={{ width: 220 }}
value={cond.value2 || ''}
onChange={e => updateFilter(cond.id, 'value2', e.target.value)}
@@ -5214,9 +5244,10 @@ const DataGrid: React.FC<DataGridProps> = ({
/>
</>
) : isNoValueOp(cond.op) ? (
<Input style={{ width: 220 }} value="" disabled placeholder="无需输入值" />
<Input {...noAutoCapInputProps} style={{ width: 220 }} value="" disabled placeholder="无需输入值" />
) : (
<Input
{...noAutoCapInputProps}
style={{ width: 280 }}
value={cond.value}
onChange={e => updateFilter(cond.id, 'value', e.target.value)}

View File

@@ -5,7 +5,7 @@ import { useStore } from '../store';
import { DBGetDatabases, DBGetTables, DataSync, DataSyncAnalyze, DataSyncPreview } from '../../wailsjs/go/app/App';
import { SavedConnection } from '../types';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues, resolveTextInputSafeBackdropFilter } from '../utils/appearance';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { formatLocalDateTimeLiteral, normalizeTemporalLiteralText } from './dataGridCopyInsert';
@@ -190,6 +190,7 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open,
const darkMode = themeMode === 'dark';
const resolvedAppearance = resolveAppearanceValues(appearance);
const effectiveOpacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
const disableLocalBackdropFilter = isMacLikePlatform();
// Step 1: Config
const [sourceConnId, setSourceConnId] = useState<string>('');
@@ -630,8 +631,8 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open,
: 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(246,248,252,0.98) 100%)',
border: darkMode ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(16,24,40,0.08)',
boxShadow: darkMode ? '0 24px 56px rgba(0,0,0,0.36)' : '0 18px 44px rgba(15,23,42,0.14)',
backdropFilter: darkMode ? 'blur(18px)' : 'none',
}), [darkMode]);
backdropFilter: resolveTextInputSafeBackdropFilter(darkMode ? 'blur(18px)' : 'none', disableLocalBackdropFilter),
}), [darkMode, disableLocalBackdropFilter]);
const shellCardStyle = useMemo<React.CSSProperties>(() => ({
borderRadius: 18,

View File

@@ -6,6 +6,7 @@ import { quoteIdentPart, escapeLiteral } from '../utils/sql';
import { useStore } from '../store';
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { isMacLikePlatform } from '../utils/appearance';
interface FindInDatabaseModalProps {
open: boolean;
@@ -67,14 +68,15 @@ const FindInDatabaseModal: React.FC<FindInDatabaseModalProps> = ({ open, onClose
const connections = useStore(state => state.connections);
const theme = useStore(state => state.theme);
const disableLocalBackdropFilter = isMacLikePlatform();
const conn = useMemo(() => connections.find(c => c.id === connectionId), [connections, connectionId]);
const dbType = useMemo(() => (conn?.config?.type || 'mysql').toLowerCase(), [conn]);
const wt = useMemo(() => {
const isDark = theme === 'dark';
return buildOverlayWorkbenchTheme(isDark);
}, [theme]);
return buildOverlayWorkbenchTheme(isDark, { disableBackdropFilter: disableLocalBackdropFilter });
}, [disableLocalBackdropFilter, theme]);
const buildConfig = useCallback(() => {
if (!conn) return null;

View File

@@ -6,7 +6,14 @@ import { useStore } from '../store';
import { RedisKeyInfo, RedisValue, StreamEntry } from '../types';
import Editor from '@monaco-editor/react';
import type { DataNode } from 'antd/es/tree';
import { blurToFilter, normalizeBlurForPlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
import {
blurToFilter,
isMacLikePlatform,
normalizeBlurForPlatform,
normalizeOpacityForPlatform,
resolveAppearanceValues,
resolveTextInputSafeBackdropFilter,
} from '../utils/appearance';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import {
applyRenamedRedisKeyState,
@@ -19,6 +26,8 @@ import {
type RedisTreeDataNode,
} from './redisViewerTree';
import { buildRedisWorkbenchTheme } from './redisViewerWorkbenchTheme';
import { noAutoCapInputProps } from '../utils/inputAutoCap';
import { normalizeRedisSearchDraftChange, normalizeRedisSearchInput } from '../utils/redisSearchPattern';
const { Search } = Input;
@@ -283,8 +292,16 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
const resolvedAppearance = resolveAppearanceValues(appearance);
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
const blur = normalizeBlurForPlatform(resolvedAppearance.blur);
const disableLocalBackdropFilter = isMacLikePlatform();
const connection = connections.find(c => c.id === connectionId);
const workbenchTheme = useMemo(() => buildRedisWorkbenchTheme({ darkMode, opacity, blur }), [blur, darkMode, opacity]);
const workbenchTheme = useMemo(
() => buildRedisWorkbenchTheme({ darkMode, opacity, blur, disableBackdropFilter: disableLocalBackdropFilter }),
[blur, darkMode, disableLocalBackdropFilter, opacity],
);
const workbenchBackdropFilter = useMemo(
() => resolveTextInputSafeBackdropFilter(blurToFilter(blur), disableLocalBackdropFilter),
[blur, disableLocalBackdropFilter],
);
const keyAccentColor = workbenchTheme.accent;
const jsonAccentColor = darkMode ? '#f6c453' : '#1890ff';
const valueToolbarBg = workbenchTheme.panelBgStrong;
@@ -293,6 +310,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
const [keys, setKeys] = useState<RedisKeyInfo[]>([]);
const [loading, setLoading] = useState(false);
const [searchInput, setSearchInput] = useState('');
const [searchPattern, setSearchPattern] = useState('*');
const [cursor, setCursor] = useState<string>('0');
const [hasMore, setHasMore] = useState(false);
@@ -467,13 +485,29 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
useEffect(() => {
loadKeys(searchPattern, '0', false, getRedisScanLoadCount(searchPattern, false));
}, [redisDB]);
}, [loadKeys, redisDB]);
const executeSearch = useCallback((value: string) => {
const normalized = normalizeRedisSearchInput(value);
setSearchInput(normalized.keyword);
setSearchPattern(normalized.pattern);
setCursor('0');
loadKeys(normalized.pattern, '0', false, getRedisScanLoadCount(normalized.pattern, false));
}, [loadKeys]);
const handleSearch = (value: string) => {
const pattern = value.trim() || '*';
setSearchPattern(pattern);
executeSearch(value);
};
const handleSearchInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const normalized = normalizeRedisSearchDraftChange(event.target.value);
setSearchInput(normalized.keyword);
if (!normalized.shouldSearchImmediately) {
return;
}
setSearchPattern(normalized.pattern);
setCursor('0');
loadKeys(pattern, '0', false, getRedisScanLoadCount(pattern, false));
loadKeys(normalized.pattern, '0', false, getRedisScanLoadCount(normalized.pattern, false));
};
const handleLoadMore = () => {
@@ -1214,9 +1248,9 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
title: '添加字段',
content: (
<Form id="add-hash-field-form" layout="vertical">
<Form.Item label="字段名" name="field" rules={[{ required: true }]}>
<Input id="new-hash-field" />
</Form.Item>
<Form.Item label="字段名" name="field" rules={[{ required: true }]}>
<Input id="new-hash-field" {...noAutoCapInputProps} />
</Form.Item>
<Form.Item label="值" name="value" rules={[{ required: true }]}>
<Input.TextArea id="new-hash-value" rows={4} />
</Form.Item>
@@ -1888,7 +1922,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
<div>
<div style={{ marginBottom: 8 }}>
<label>ID *</label>
<Input id="new-stream-id" placeholder="例如: * 或 1723110000000-0" />
<Input id="new-stream-id" {...noAutoCapInputProps} placeholder="例如: * 或 1723110000000-0" />
</div>
<div>
<label> JSON</label>
@@ -2050,7 +2084,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
}
return (
<div className="redis-viewer-workbench" style={{ display: 'flex', height: '100%', gap: 12, padding: 12, background: workbenchTheme.appBg, backdropFilter: blurToFilter(blur), WebkitBackdropFilter: blurToFilter(blur) }}>
<div className="redis-viewer-workbench" style={{ display: 'flex', height: '100%', gap: 12, padding: 12, background: workbenchTheme.appBg, backdropFilter: workbenchBackdropFilter, WebkitBackdropFilter: workbenchBackdropFilter }}>
{/* Left: Key List */}
<div ref={leftPanelRef} style={{ width: leftPanelWidth, minWidth: 300, display: 'flex', flexDirection: 'column', flexShrink: 0, gap: 12 }}>
<div style={{ ...workbenchCardStyle, padding: 12 }}>
@@ -2063,9 +2097,12 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
</div>
<Space.Compact style={{ width: '100%' }}>
<Search
placeholder="搜索 Key (支持 * 通配符)"
defaultValue="*"
{...noAutoCapInputProps}
placeholder="搜索 Key"
value={searchInput}
onChange={handleSearchInputChange}
onSearch={handleSearch}
allowClear
enterButton={<SearchOutlined />}
/>
</Space.Compact>
@@ -2177,7 +2214,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
>
<Form form={newKeyForm} layout="vertical" initialValues={{ ttl: -1 }}>
<Form.Item name="key" label="Key" rules={[{ required: true, message: '请输入 Key' }]}>
<Input placeholder="key name" />
<Input {...noAutoCapInputProps} placeholder="key name" />
</Form.Item>
<Form.Item name="value" label="值" rules={[{ required: true, message: '请输入值' }]}>
<Input.TextArea rows={4} placeholder="value" />
@@ -2207,7 +2244,7 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
rules={[{ required: true, message: '请输入新的 Key 名称' }]}
extra={renameTargetKey ? `原始 Key${renameTargetKey}` : undefined}
>
<Input placeholder="new:key:name" />
<Input {...noAutoCapInputProps} placeholder="new:key:name" />
</Form.Item>
</Form>
</Modal>

View File

@@ -41,11 +41,13 @@ import { getDbIcon } from './DatabaseIcons';
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, ExportTable, OpenSQLFile, ExecuteSQLFile, CancelSQLFileExecution, CreateDatabase, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView } from '../../wailsjs/go/app/App';
import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDataDangerActionKind } from './tableDataDangerActions';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
import FindInDatabaseModal from './FindInDatabaseModal';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { noAutoCapInputProps } from '../utils/inputAutoCap';
import { normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata';
import { resolveConnectionHostTokens } from '../utils/tabDisplay';
const { Search } = Input;
@@ -138,6 +140,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
const darkMode = theme === 'dark';
const resolvedAppearance = resolveAppearanceValues(appearance);
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
const disableLocalBackdropFilter = isMacLikePlatform();
const autoFetchVisible = useAutoFetchVisibility();
const [treeData, setTreeData] = useState<TreeNode[]>([]);
@@ -151,7 +154,10 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
};
const bgMain = getBg('#141414');
const overlayTheme = useMemo(() => buildOverlayWorkbenchTheme(darkMode), [darkMode]);
const overlayTheme = useMemo(
() => buildOverlayWorkbenchTheme(darkMode, { disableBackdropFilter: disableLocalBackdropFilter }),
[darkMode, disableLocalBackdropFilter],
);
const modalPanelStyle = useMemo(() => ({
background: overlayTheme.shellBg,
border: overlayTheme.shellBorder,
@@ -2858,51 +2864,10 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
);
}, [darkMode, overlayTheme, searchScopes]);
const parseHostOnlyToken = (value: unknown): string[] => {
const raw = String(value || '').trim();
if (!raw) {
return [];
}
let text = raw.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
if (text.includes('/')) {
text = text.split('/')[0];
}
if (text.includes('?')) {
text = text.split('?')[0];
}
if (text.includes('@')) {
text = text.split('@').pop() || '';
}
return text
.split(',')
.map((entry) => {
const token = entry.trim();
if (!token) return '';
if (token.startsWith('[')) {
const rightBracketIndex = token.indexOf(']');
if (rightBracketIndex > 0) {
return token.slice(0, rightBracketIndex + 1).toLowerCase();
}
}
const colonIndex = token.lastIndexOf(':');
if (colonIndex > 0) {
return token.slice(0, colonIndex).toLowerCase();
}
return token.toLowerCase();
})
.filter(Boolean);
};
const getConnectionHostSearchText = (node: TreeNode): string => {
if (node.type !== 'connection') return '';
const config = node.dataRef?.config || {};
const hostTokens = [
...parseHostOnlyToken(config.host),
...(Array.isArray(config.hosts) ? config.hosts.flatMap((entry: string) => parseHostOnlyToken(entry)) : []),
...parseHostOnlyToken(config.uri),
];
const uniqueHosts = Array.from(new Set(hostTokens));
return uniqueHosts.join(' ');
return resolveConnectionHostTokens(config).join(' ');
};
const getConnectionNameSearchText = (node: TreeNode): string => {
@@ -3110,7 +3075,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
onClick: () => {
addTab({
id: `redis-cmd-${node.key}-${Date.now()}`,
title: `命令 - ${node.title}`,
title: '命令 - db0',
type: 'redis-command',
connectionId: node.key,
redisDB: 0
@@ -3124,7 +3089,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
onClick: () => {
addTab({
id: `redis-monitor-${node.key}-${Date.now()}`,
title: `监控: ${node.title}`,
title: '监控 - db0',
type: 'redis-monitor',
connectionId: node.key,
redisDB: 0
@@ -3386,7 +3351,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
onClick: () => {
addTab({
id: `redis-monitor-${id}-db${redisDB}-${Date.now()}`,
title: `监控: ${connections.find(c => c.id === id)?.name || id}`,
title: `监控 - db${redisDB}`,
type: 'redis-monitor',
connectionId: id,
redisDB: redisDB
@@ -3835,6 +3800,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ padding: '8px 14px', borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}` }}>
<Input
{...noAutoCapInputProps}
ref={searchInputRef}
placeholder="搜索..."
onChange={onSearch}
@@ -4026,7 +3992,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
>
<Form form={createDbForm} layout="vertical">
<Form.Item name="name" label="数据库名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input />
<Input {...noAutoCapInputProps} />
</Form.Item>
{/* Charset option could be added here */}
</Form>
@@ -4044,7 +4010,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
>
<Form form={renameDbForm} layout="vertical">
<Form.Item name="newName" label="新数据库名称" rules={[{ required: true, message: '请输入新数据库名称' }]}>
<Input />
<Input {...noAutoCapInputProps} />
</Form.Item>
</Form>
</Modal>
@@ -4061,7 +4027,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
>
<Form form={renameTableForm} layout="vertical">
<Form.Item name="newName" label="新表名" rules={[{ required: true, message: '请输入新表名' }]}>
<Input />
<Input {...noAutoCapInputProps} />
</Form.Item>
</Form>
</Modal>
@@ -4078,7 +4044,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
>
<Form form={renameViewForm} layout="vertical">
<Form.Item name="newName" label="新视图名" rules={[{ required: true, message: '请输入新视图名' }]}>
<Input />
<Input {...noAutoCapInputProps} />
</Form.Item>
</Form>
</Modal>

View File

@@ -17,24 +17,7 @@ import TriggerViewer from './TriggerViewer';
import DefinitionViewer from './DefinitionViewer';
import TableOverview from './TableOverview';
import type { TabData } from '../types';
const detectConnectionEnvLabel = (connectionName: string): string | null => {
const tokens = connectionName.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
if (tokens.includes('prod') || tokens.includes('production')) return 'PROD';
if (tokens.includes('uat')) return 'UAT';
if (tokens.includes('dev') || tokens.includes('development')) return 'DEV';
if (tokens.includes('sit')) return 'SIT';
if (tokens.includes('stg') || tokens.includes('stage') || tokens.includes('staging') || tokens.includes('pre')) return 'STG';
if (tokens.includes('test') || tokens.includes('qa')) return 'TEST';
return null;
};
const buildTabDisplayTitle = (tab: TabData, connectionName: string | undefined): string => {
if (tab.type !== 'table' && tab.type !== 'design' && tab.type !== 'table-overview') return tab.title;
if (!connectionName) return tab.title;
const prefix = detectConnectionEnvLabel(connectionName) || connectionName;
return `[${prefix}] ${tab.title}`;
};
import { buildTabDisplayTitle } from '../utils/tabDisplay';
type SortableTabLabelProps = {
displayTitle: string;
@@ -50,7 +33,7 @@ const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
<span
className="tab-dnd-label"
onContextMenu={(e) => e.preventDefault()}
title="拖拽调整标签顺序"
title={displayTitle}
>
{displayTitle}
</span>
@@ -198,8 +181,8 @@ const TabManager: React.FC = () => {
);
const items = useMemo(() => tabs.map((tab, index) => {
const connectionName = connections.find((conn) => conn.id === tab.connectionId)?.name;
const displayTitle = buildTabDisplayTitle(tab, connectionName);
const connection = connections.find((conn) => conn.id === tab.connectionId);
const displayTitle = buildTabDisplayTitle(tab, connection);
const tabIsActive = tab.id === activeTabId;
let content;
if (tab.type === 'query') {

View File

@@ -11,6 +11,7 @@ import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, D
import { hasIndexFormChanged, normalizeIndexFormFromRow, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils';
import { buildAlterTablePreviewSql } from './tableDesignerSchemaSql';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { noAutoCapInputProps } from '../utils/inputAutoCap';
interface EditableColumn extends ColumnDefinition {
_key: string;
@@ -546,7 +547,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
key: 'name',
width: 180,
render: (text: string, record: EditableColumn) => readOnly ? text : (
<Input value={text} onChange={e => handleColumnChange(record._key, 'name', e.target.value)} variant="borderless" />
<Input {...noAutoCapInputProps} value={text} onChange={e => handleColumnChange(record._key, 'name', e.target.value)} variant="borderless" />
)
},
{
@@ -2492,6 +2493,7 @@ END;`;
{isNewTable && (
<>
<Input
{...noAutoCapInputProps}
placeholder="请输入表名"
value={newTableName}
onChange={e => setNewTableName(e.target.value)}
@@ -2805,6 +2807,7 @@ END;`;
{selectedColumns.length}
</div>
<Input
{...noAutoCapInputProps}
placeholder="请输入目标表名"
value={copyTableName}
onChange={e => setCopyTableName(e.target.value)}
@@ -2865,6 +2868,7 @@ END;`;
>
<Space direction="vertical" size={10} style={{ width: '100%' }}>
<Input
{...noAutoCapInputProps}
placeholder={indexForm.kind === 'PRIMARY' ? '主键索引固定名称PRIMARY' : '索引名(例如 idx_user_name'}
value={indexForm.name}
onChange={(e) => setIndexForm(prev => ({ ...prev, name: e.target.value }))}
@@ -2934,6 +2938,7 @@ END;`;
>
<Space direction="vertical" size={10} style={{ width: '100%' }}>
<Input
{...noAutoCapInputProps}
placeholder="外键约束名(例如 fk_order_user"
value={foreignKeyForm.constraintName}
onChange={(e) => setForeignKeyForm(prev => ({ ...prev, constraintName: e.target.value }))}
@@ -2949,6 +2954,7 @@ END;`;
style={{ width: '100%' }}
/>
<Input
{...noAutoCapInputProps}
placeholder="参考表(支持 db.table"
value={foreignKeyForm.refTableName}
onChange={(e) => setForeignKeyForm(prev => ({ ...prev, refTableName: e.target.value }))}

View File

@@ -6,6 +6,7 @@ import { DBQuery, DBShowCreateTable, ExportTable, DropTable, RenameTable } from
import type { TabData } from '../types';
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { noAutoCapInputProps } from '../utils/inputAutoCap';
import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDataDangerActionKind } from './tableDataDangerActions';
interface TableOverviewProps {
@@ -344,6 +345,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
title: '重命名表',
content: (
<Input
{...noAutoCapInputProps}
defaultValue={tableName}
onChange={e => { newName = e.target.value; }}
placeholder="输入新表名"
@@ -417,6 +419,7 @@ const TableOverview: React.FC<TableOverviewProps> = ({ tab }) => {
</span>
<div style={{ flex: 1 }} />
<Input
{...noAutoCapInputProps}
placeholder="搜索表名或注释..."
prefix={<SearchOutlined style={{ color: textMuted }} />}
value={searchText}

View File

@@ -25,4 +25,9 @@ describe('buildRedisWorkbenchTheme', () => {
expect(lightTheme.statusTagBg).not.toBe(lightTheme.statusTagMutedBg);
expect(lightTheme.backdropFilter).toBe('none');
});
it('can disable redis workbench blur for macOS text-entry compatibility', () => {
const darkTheme = buildRedisWorkbenchTheme({ darkMode: true, opacity: 0.72, blur: 14, disableBackdropFilter: true });
expect(darkTheme.backdropFilter).toBe('none');
});
});

View File

@@ -1,7 +1,10 @@
import { resolveTextInputSafeBackdropFilter } from '../utils/appearance';
type RedisWorkbenchThemeInput = {
darkMode: boolean;
opacity: number;
blur: number;
disableBackdropFilter?: boolean;
};
type RedisWorkbenchTheme = {
@@ -43,10 +46,15 @@ export const buildRedisWorkbenchTheme = ({
darkMode,
opacity,
blur,
disableBackdropFilter,
}: RedisWorkbenchThemeInput): RedisWorkbenchTheme => {
const normalizedOpacity = clamp(opacity, 0.1, 1);
const normalizedBlur = Math.max(0, Math.round(blur));
const isTranslucent = normalizedOpacity < 0.999 || normalizedBlur > 0;
const backdropFilter = resolveTextInputSafeBackdropFilter(
normalizedBlur > 0 ? `blur(${normalizedBlur}px)` : 'none',
disableBackdropFilter ?? false,
);
if (darkMode) {
const appTopAlpha = isTranslucent ? Math.max(0.08, Math.min(0.22, normalizedOpacity * 0.16)) : 0.92;
@@ -84,7 +92,7 @@ export const buildRedisWorkbenchTheme = ({
treeSelectedBorder: 'rgba(246, 196, 83, 0.24)',
divider: 'rgba(255, 255, 255, 0.07)',
shadow: '0 20px 48px rgba(0, 0, 0, 0.26)',
backdropFilter: normalizedBlur > 0 ? `blur(${normalizedBlur}px)` : 'none',
backdropFilter,
};
}
@@ -122,7 +130,7 @@ export const buildRedisWorkbenchTheme = ({
treeSelectedBorder: 'rgba(22, 119, 255, 0.18)',
divider: 'rgba(15, 23, 42, 0.08)',
shadow: '0 22px 52px rgba(15, 23, 42, 0.08)',
backdropFilter: normalizedBlur > 0 ? `blur(${normalizedBlur}px)` : 'none',
backdropFilter,
};
};