import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Table, Input, Button, Space, Tag, Tree, Spin, message, Modal, Form, InputNumber, Popconfirm, Tooltip, Radio } from 'antd'; import { ReloadOutlined, DeleteOutlined, PlusOutlined, EditOutlined, SearchOutlined, ClockCircleOutlined, CopyOutlined, FolderOpenOutlined, KeyOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { RedisKeyInfo, RedisValue, StreamEntry } from '../types'; import Editor from '@monaco-editor/react'; import type { DataNode } from 'antd/es/tree'; const { Search } = Input; const KEY_GROUP_DELIMITER = ':'; const EMPTY_SEGMENT_LABEL = '(empty)'; const REDIS_TREE_KEY_TYPE_WIDTH = 92; const REDIS_TREE_KEY_TYPE_WIDTH_NARROW = 84; const REDIS_TREE_KEY_TTL_WIDTH = 92; const REDIS_TREE_HIDE_TTL_THRESHOLD = 460; const REDIS_KEY_INITIAL_LOAD_COUNT = 2000; const REDIS_KEY_LOAD_MORE_COUNT = 2000; const REDIS_KEY_SEARCH_INITIAL_LOAD_COUNT = 600; const REDIS_KEY_SEARCH_LOAD_MORE_COUNT = 1000; const REDIS_LARGE_KEYSPACE_THRESHOLD = 10000; const REDIS_LARGE_KEYSPACE_MAX_EXPANDED_GROUPS = 200; interface RedisViewerProps { connectionId: string; redisDB: number; } // 尝试多种方式解码二进制数据 const tryDecodeValue = (value: string): { displayValue: string; encoding: string; needsHex: boolean } => { if (!value || value.length === 0) { return { displayValue: '', encoding: 'UTF-8', needsHex: false }; } // 统计字节分布 let nullCount = 0; let printableCount = 0; let highByteCount = 0; const sampleSize = Math.min(value.length, 200); for (let i = 0; i < sampleSize; i++) { const code = value.charCodeAt(i); if (code === 0) { nullCount++; } else if (code >= 32 && code < 127) { printableCount++; } else if (code >= 128) { highByteCount++; } } // 如果超过30%是null字节,很可能是二进制数据,显示十六进制 if (nullCount / sampleSize > 0.3) { return { displayValue: toHexDisplay(value), encoding: 'HEX', needsHex: true }; } // 如果超过70%是可打印ASCII字符,直接显示 if (printableCount / sampleSize > 0.7) { return { displayValue: value, encoding: 'UTF-8', needsHex: false }; } // 尝试UTF-8解码 if (highByteCount > 0) { try { const bytes = new Uint8Array(value.length); for (let i = 0; i < value.length; i++) { bytes[i] = value.charCodeAt(i) & 0xFF; } const decoded = new TextDecoder('utf-8', { fatal: false }).decode(bytes); // 检查解码质量 let validChars = 0; let replacementChars = 0; let controlChars = 0; for (let i = 0; i < Math.min(decoded.length, 200); i++) { const code = decoded.charCodeAt(i); if (code === 0xFFFD) { replacementChars++; } else if (code < 32 && code !== 9 && code !== 10 && code !== 13) { controlChars++; } else if ((code >= 32 && code < 127) || (code >= 0x4E00 && code <= 0x9FFF) || (code >= 0x3000 && code <= 0x303F)) { // ASCII可打印字符、中文字符、中文标点 validChars++; } } const totalChecked = Math.min(decoded.length, 200); // 如果替换字符超过10%或控制字符超过20%,说明不是有效的UTF-8文本 if (replacementChars / totalChecked > 0.1 || controlChars / totalChecked > 0.2) { return { displayValue: toHexDisplay(value), encoding: 'HEX', needsHex: true }; } // 如果有效字符超过50%,使用UTF-8解码 if (validChars / totalChecked > 0.5) { return { displayValue: decoded, encoding: 'UTF-8', needsHex: false }; } } catch (e) { // UTF-8解码失败 } } // 默认显示十六进制 return { displayValue: toHexDisplay(value), encoding: 'HEX', needsHex: true }; }; // 检测是否为二进制数据(包含大量不可打印字符) const isBinaryData = (value: string): boolean => { if (!value || value.length === 0) return false; // 检查前 100 个字符中不可打印字符的比例 const sampleSize = Math.min(value.length, 100); let nonPrintableCount = 0; for (let i = 0; i < sampleSize; i++) { const code = value.charCodeAt(i); // 不可打印字符:控制字符(0-31,除了 9, 10, 13)和 DEL(127) if ((code < 32 && code !== 9 && code !== 10 && code !== 13) || code === 127 || code > 255) { nonPrintableCount++; } } // 如果超过 10% 是不可打印字符,认为是二进制数据 return nonPrintableCount / sampleSize > 0.1; }; // 将字符串转换为十六进制显示 const toHexDisplay = (value: string): string => { const bytes: string[] = []; const ascii: string[] = []; let result = ''; for (let i = 0; i < value.length; i++) { const code = value.charCodeAt(i); bytes.push(code.toString(16).padStart(2, '0').toUpperCase()); // 可打印 ASCII 字符显示原字符,否则显示点 ascii.push(code >= 32 && code < 127 ? value[i] : '.'); if (bytes.length === 16 || i === value.length - 1) { const offset = (Math.floor(i / 16) * 16).toString(16).padStart(8, '0').toUpperCase(); const hexPart = bytes.join(' ').padEnd(47, ' '); const asciiPart = ascii.join(''); result += `${offset} ${hexPart} |${asciiPart}|\n`; bytes.length = 0; ascii.length = 0; } } return result; }; // 尝试解析并格式化 JSON const tryFormatJson = (value: string): { isJson: boolean; formatted: string } => { try { const parsed = JSON.parse(value); return { isJson: true, formatted: JSON.stringify(parsed, null, 2) }; } catch { return { isJson: false, formatted: value }; } }; // 格式化字符串值 - 支持 JSON、二进制数据检测和智能解码 const formatStringValue = (value: string): { displayValue: string; isBinary: boolean; isJson: boolean; encoding?: string } => { // 先检测是否为二进制数据 if (isBinaryData(value)) { const { displayValue, encoding, needsHex } = tryDecodeValue(value); return { displayValue, isBinary: needsHex, isJson: false, encoding }; } // 尝试 JSON 格式化 const { isJson, formatted } = tryFormatJson(value); return { displayValue: formatted, isBinary: false, isJson, encoding: 'UTF-8' }; }; // 可拖拽分隔条组件 - 使用直接 DOM 操作避免卡顿 const ResizableDivider: React.FC<{ onResizeEnd: (newWidth: number) => void; targetRef: React.RefObject; minWidth?: number; }> = ({ onResizeEnd, targetRef, minWidth = 300 }) => { const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); const target = targetRef.current; if (!target) return; const startX = e.clientX; const startWidth = target.offsetWidth; const containerWidth = target.parentElement?.offsetWidth || window.innerWidth; const maxWidth = containerWidth - 350; // 右侧至少留 350px // 创建遮罩层防止文本选择和其他交互 const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;cursor:col-resize;z-index:9999;'; document.body.appendChild(overlay); let currentWidth = startWidth; const handleMouseMove = (moveEvent: MouseEvent) => { moveEvent.preventDefault(); const delta = moveEvent.clientX - startX; currentWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + delta)); // 直接操作 DOM,不触发 React 重渲染 target.style.width = `${currentWidth}px`; target.style.flexBasis = `${currentWidth}px`; }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.body.removeChild(overlay); // 拖拽结束时才更新 React state onResizeEnd(currentWidth); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; return (
(e.currentTarget.style.background = '#d9d9d9')} onMouseLeave={(e) => (e.currentTarget.style.background = '#f0f0f0')} >
); }; // 可拖拽列头组件 - 纯 DOM 操作实现 type RedisKeyTreeLeaf = { keyInfo: RedisKeyInfo; label: string; }; type RedisKeyTreeGroup = { name: string; path: string; children: Map; leaves: RedisKeyTreeLeaf[]; leafCount: number; }; type RedisKeyTreeResult = { treeData: RedisTreeDataNode[]; groupKeys: string[]; }; type RedisTreeDataNode = DataNode & { nodeType: 'group' | 'leaf'; groupName?: string; groupLeafCount?: number; leafLabel?: string; rawKey?: string; keyType?: string; ttl?: number; }; const buildLeafNodeKey = (rawKey: string): string => `key:${rawKey}`; const parseRawKeyFromNodeKey = (nodeKey: React.Key): string | null => { const keyText = String(nodeKey); if (!keyText.startsWith('key:')) { return null; } return keyText.slice(4); }; const getRedisScanLoadCount = (pattern: string, append: boolean): number => { const normalizedPattern = pattern.trim() || '*'; if (normalizedPattern === '*') { return append ? REDIS_KEY_LOAD_MORE_COUNT : REDIS_KEY_INITIAL_LOAD_COUNT; } return append ? REDIS_KEY_SEARCH_LOAD_MORE_COUNT : REDIS_KEY_SEARCH_INITIAL_LOAD_COUNT; }; const normalizeKeySegment = (segment: string): string => { return segment === '' ? EMPTY_SEGMENT_LABEL : segment; }; const createTreeGroup = (name: string, path: string): RedisKeyTreeGroup => { return { name, path, children: new Map(), leaves: [], leafCount: 0 }; }; const calculateGroupLeafCount = (group: RedisKeyTreeGroup): number => { let count = group.leaves.length; group.children.forEach((child) => { count += calculateGroupLeafCount(child); }); group.leafCount = count; return count; }; const buildRedisKeyTree = ( keys: RedisKeyInfo[], sortLeafNodes: boolean ): RedisKeyTreeResult => { const root = createTreeGroup('__root__', '__root__'); keys.forEach((keyInfo) => { const segments = keyInfo.key.split(KEY_GROUP_DELIMITER); if (segments.length <= 1) { root.leaves.push({ keyInfo, label: keyInfo.key }); return; } const groupSegments = segments.slice(0, -1); const leafLabel = normalizeKeySegment(segments[segments.length - 1]); let current = root; const pathParts: string[] = []; groupSegments.forEach((segment) => { const normalized = normalizeKeySegment(segment); pathParts.push(normalized); const groupPath = pathParts.join(KEY_GROUP_DELIMITER); let child = current.children.get(normalized); if (!child) { child = createTreeGroup(normalized, groupPath); current.children.set(normalized, child); } current = child; }); current.leaves.push({ keyInfo, label: leafLabel }); }); calculateGroupLeafCount(root); const groupKeys: string[] = []; const toTreeNodes = (group: RedisKeyTreeGroup): RedisTreeDataNode[] => { const childGroups = Array.from(group.children.values()).sort((a, b) => a.name.localeCompare(b.name)); const childLeaves = sortLeafNodes ? [...group.leaves].sort((a, b) => a.keyInfo.key.localeCompare(b.keyInfo.key)) : group.leaves; const groupNodes: RedisTreeDataNode[] = childGroups.map((child) => { const groupNodeKey = `group:${child.path}`; groupKeys.push(groupNodeKey); return { key: groupNodeKey, title: child.name, nodeType: 'group', groupName: child.name, groupLeafCount: child.leafCount, selectable: false, disableCheckbox: true, children: toTreeNodes(child), }; }); const leafNodes: RedisTreeDataNode[] = childLeaves.map((leaf) => { return { key: buildLeafNodeKey(leaf.keyInfo.key), isLeaf: true, title: leaf.label, nodeType: 'leaf', leafLabel: leaf.label, rawKey: leaf.keyInfo.key, keyType: leaf.keyInfo.type, ttl: leaf.keyInfo.ttl, }; }); return [...groupNodes, ...leafNodes]; }; return { treeData: toTreeNodes(root), groupKeys, }; }; const RedisViewer: React.FC = ({ connectionId, redisDB }) => { const { connections } = useStore(); const connection = connections.find(c => c.id === connectionId); const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(false); const [searchPattern, setSearchPattern] = useState('*'); const [cursor, setCursor] = useState(0); const [hasMore, setHasMore] = useState(false); const [selectedKey, setSelectedKey] = useState(null); const [keyValue, setKeyValue] = useState(null); const [valueLoading, setValueLoading] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); const [newKeyModalOpen, setNewKeyModalOpen] = useState(false); const [newKeyForm] = Form.useForm(); const [ttlModalOpen, setTtlModalOpen] = useState(false); const [ttlForm] = Form.useForm(); const [selectedKeys, setSelectedKeys] = useState([]); const [editValue, setEditValue] = useState(''); // 视图模式状态(用于所有数据类型) const [viewMode, setViewMode] = useState<'auto' | 'text' | 'utf8' | 'hex'>('auto'); // JSON 编辑弹窗状态 const [jsonEditModalOpen, setJsonEditModalOpen] = useState(false); const [jsonEditConfig, setJsonEditConfig] = useState<{ title: string; value: string; isJson: boolean; onSave: (newValue: string) => Promise; } | null>(null); const jsonEditValueRef = useRef(''); const latestLoadRequestIdRef = useRef(0); // 面板宽度状态和 ref - 默认占据 50% 宽度 const [leftPanelWidth, setLeftPanelWidth] = useState('50%'); const leftPanelRef = useRef(null); const treeContainerRef = useRef(null); const [showTreeKeyTTL, setShowTreeKeyTTL] = useState(true); const [treeHeight, setTreeHeight] = useState(500); const [expandedGroupKeys, setExpandedGroupKeys] = useState([]); const getConfig = useCallback(() => { if (!connection) return null; return { ...connection.config, port: Number(connection.config.port), password: connection.config.password || "", useSSH: connection.config.useSSH || false, ssh: connection.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" }, redisDB: redisDB }; }, [connection, redisDB]); const loadKeys = useCallback(async ( pattern: string = '*', fromCursor: number = 0, append: boolean = false, targetCount?: number ) => { const config = getConfig(); if (!config) return; const normalizedPattern = pattern.trim() || '*'; const effectiveTargetCount = targetCount ?? getRedisScanLoadCount(normalizedPattern, append); const requestId = latestLoadRequestIdRef.current + 1; latestLoadRequestIdRef.current = requestId; setLoading(true); try { const res = await (window as any).go.app.App.RedisScanKeys(config, normalizedPattern, fromCursor, effectiveTargetCount); if (requestId !== latestLoadRequestIdRef.current) { return; } if (res.success) { const result = res.data; const scannedKeys = Array.isArray(result?.keys) ? result.keys : []; const nextCursor = Number(result?.cursor || 0); if (append) { setKeys(prev => { const keyMap = new Map(); prev.forEach(item => keyMap.set(item.key, item)); scannedKeys.forEach((item: RedisKeyInfo) => keyMap.set(item.key, item)); return Array.from(keyMap.values()); }); } else { setKeys(scannedKeys); } setCursor(nextCursor); setHasMore(nextCursor !== 0); } else { message.error('加载 Key 失败: ' + res.message); } } catch (e: any) { if (requestId !== latestLoadRequestIdRef.current) { return; } message.error('加载 Key 失败: ' + (e?.message || String(e))); } finally { if (requestId === latestLoadRequestIdRef.current) { setLoading(false); } } }, [getConfig]); useEffect(() => { loadKeys(searchPattern, 0, false, getRedisScanLoadCount(searchPattern, false)); }, [redisDB]); const handleSearch = (value: string) => { const pattern = value.trim() || '*'; setSearchPattern(pattern); setCursor(0); loadKeys(pattern, 0, false, getRedisScanLoadCount(pattern, false)); }; const handleLoadMore = () => { if (!hasMore || loading) { return; } loadKeys(searchPattern, cursor, true, getRedisScanLoadCount(searchPattern, true)); }; const handleRefresh = () => { setCursor(0); loadKeys(searchPattern, 0, false, getRedisScanLoadCount(searchPattern, false)); }; const loadKeyValue = async (key: string) => { const config = getConfig(); if (!config) return; setValueLoading(true); try { const res = await (window as any).go.app.App.RedisGetValue(config, key); if (res.success) { setKeyValue(res.data); setSelectedKey(key); } else { message.error('获取值失败: ' + res.message); } } catch (e: any) { message.error('获取值失败: ' + (e?.message || String(e))); } finally { setValueLoading(false); } }; const handleDeleteKeys = async (keysToDelete: string[]) => { const config = getConfig(); if (!config) return; try { const res = await (window as any).go.app.App.RedisDeleteKeys(config, keysToDelete); if (res.success) { message.success(`已删除 ${res.data.deleted} 个 Key`); setKeys(prev => prev.filter(k => !keysToDelete.includes(k.key))); if (selectedKey && keysToDelete.includes(selectedKey)) { setSelectedKey(null); setKeyValue(null); } setSelectedKeys([]); } else { message.error('删除失败: ' + res.message); } } catch (e: any) { message.error('删除失败: ' + (e?.message || String(e))); } }; const handleDeleteCurrentKey = async () => { if (!selectedKey) return; await handleDeleteKeys([selectedKey]); }; const handleSetTTL = async () => { const config = getConfig(); if (!config || !selectedKey) return; try { const values = await ttlForm.validateFields(); const res = await (window as any).go.app.App.RedisSetTTL(config, selectedKey, values.ttl); if (res.success) { message.success('TTL 设置成功'); setTtlModalOpen(false); loadKeyValue(selectedKey); handleRefresh(); } else { message.error('设置失败: ' + res.message); } } catch (e: any) { message.error('设置失败: ' + (e?.message || String(e))); } }; const handleSaveString = async () => { const config = getConfig(); if (!config || !selectedKey) return; try { const res = await (window as any).go.app.App.RedisSetString(config, selectedKey, editValue, keyValue?.ttl || -1); if (res.success) { message.success('保存成功'); setEditModalOpen(false); loadKeyValue(selectedKey); } else { message.error('保存失败: ' + res.message); } } catch (e: any) { message.error('保存失败: ' + (e?.message || String(e))); } }; const handleCreateKey = async () => { const config = getConfig(); if (!config) return; try { const values = await newKeyForm.validateFields(); const res = await (window as any).go.app.App.RedisSetString(config, values.key, values.value, values.ttl || -1); if (res.success) { message.success('创建成功'); setNewKeyModalOpen(false); newKeyForm.resetFields(); handleRefresh(); } else { message.error('创建失败: ' + res.message); } } catch (e: any) { message.error('创建失败: ' + (e?.message || String(e))); } }; const getTypeColor = (type: string) => { switch (type) { case 'string': return 'green'; case 'hash': return 'blue'; case 'list': return 'orange'; case 'set': return 'purple'; case 'zset': return 'magenta'; case 'stream': return 'cyan'; default: return 'default'; } }; const formatTTL = (ttl: number) => { if (ttl === -1) return '永久'; if (ttl === -2) return '已过期'; if (ttl < 60) return `${ttl}秒`; if (ttl < 3600) return `${Math.floor(ttl / 60)}分${ttl % 60}秒`; if (ttl < 86400) return `${Math.floor(ttl / 3600)}时${Math.floor((ttl % 3600) / 60)}分`; return `${Math.floor(ttl / 86400)}天${Math.floor((ttl % 86400) / 3600)}时`; }; useEffect(() => { const target = leftPanelRef.current; if (!target) return; const updateTTLVisibility = (width: number) => { const nextShowTTL = width > REDIS_TREE_HIDE_TTL_THRESHOLD; setShowTreeKeyTTL((prev) => (prev === nextShowTTL ? prev : nextShowTTL)); }; updateTTLVisibility(Math.round(target.getBoundingClientRect().width)); if (typeof ResizeObserver !== 'undefined') { const observer = new ResizeObserver((entries) => { const width = Math.round(entries[0]?.contentRect.width || target.getBoundingClientRect().width); updateTTLVisibility(width); }); observer.observe(target); return () => observer.disconnect(); } const handleWindowResize = () => { updateTTLVisibility(Math.round(target.getBoundingClientRect().width)); }; window.addEventListener('resize', handleWindowResize); return () => window.removeEventListener('resize', handleWindowResize); }, []); useEffect(() => { const target = treeContainerRef.current; if (!target) return; const updateTreeHeight = (nextHeight: number) => { if (nextHeight <= 0) return; setTreeHeight((prev) => (prev === nextHeight ? prev : nextHeight)); }; updateTreeHeight(Math.round(target.getBoundingClientRect().height)); if (typeof ResizeObserver !== 'undefined') { const observer = new ResizeObserver((entries) => { const nextHeight = Math.round(entries[0]?.contentRect.height || target.getBoundingClientRect().height); updateTreeHeight(nextHeight); }); observer.observe(target); return () => observer.disconnect(); } const handleWindowResize = () => { updateTreeHeight(Math.round(target.getBoundingClientRect().height)); }; window.addEventListener('resize', handleWindowResize); return () => window.removeEventListener('resize', handleWindowResize); }, []); const isLargeKeyspace = keys.length >= REDIS_LARGE_KEYSPACE_THRESHOLD; const keyTree = useMemo(() => { return buildRedisKeyTree(keys, !isLargeKeyspace); }, [isLargeKeyspace, keys]); const groupKeySet = useMemo(() => new Set(keyTree.groupKeys), [keyTree.groupKeys]); const selectedTreeNodeKeys = useMemo(() => { if (!selectedKey) { return [] as string[]; } return [buildLeafNodeKey(selectedKey)]; }, [selectedKey]); const checkedTreeNodeKeys = useMemo(() => { return selectedKeys.map(rawKey => buildLeafNodeKey(rawKey)); }, [selectedKeys]); useEffect(() => { const existingKeySet = new Set(keys.map(item => item.key)); setSelectedKeys(prev => prev.filter(rawKey => existingKeySet.has(rawKey))); }, [keys]); useEffect(() => { setExpandedGroupKeys((prev) => { const validKeys = prev.filter(nodeKey => groupKeySet.has(nodeKey)); if (!isLargeKeyspace) { return validKeys; } return validKeys.slice(0, REDIS_LARGE_KEYSPACE_MAX_EXPANDED_GROUPS); }); }, [groupKeySet, isLargeKeyspace]); const handleTreeSelect = (nodeKeys: React.Key[]) => { if (nodeKeys.length === 0) { return; } const rawKey = parseRawKeyFromNodeKey(nodeKeys[0]); if (!rawKey) { return; } loadKeyValue(rawKey); }; const handleTreeCheck = (checked: React.Key[] | { checked: React.Key[]; halfChecked: React.Key[] }) => { const checkedNodeKeys = Array.isArray(checked) ? checked : checked.checked; const rawKeys = checkedNodeKeys .map(nodeKey => parseRawKeyFromNodeKey(nodeKey)) .filter((rawKey): rawKey is string => Boolean(rawKey)); setSelectedKeys(rawKeys); }; const renderTreeNodeTitle = useCallback((nodeData: DataNode) => { const treeNode = nodeData as RedisTreeDataNode; if (treeNode.nodeType === 'group') { return ( {treeNode.groupName} ({treeNode.groupLeafCount ?? 0}) ); } const leafLabel = treeNode.leafLabel ?? ''; const rawKey = treeNode.rawKey ?? parseRawKeyFromNodeKey(treeNode.key ?? '') ?? ''; const keyType = treeNode.keyType ?? 'unknown'; const ttl = typeof treeNode.ttl === 'number' ? treeNode.ttl : -1; if (isLargeKeyspace) { return (
{leafLabel} [{keyType}] {showTreeKeyTTL && ( {formatTTL(ttl)} )}
); } return (
{leafLabel}
{keyType} {showTreeKeyTTL && ( {formatTTL(ttl)} )}
); }, [formatTTL, getTypeColor, isLargeKeyspace, showTreeKeyTTL]); const handleTreeExpand = (nextExpandedKeys: React.Key[]) => { const validGroupKeys = nextExpandedKeys .map(key => String(key)) .filter(nodeKey => groupKeySet.has(nodeKey)); if (isLargeKeyspace) { setExpandedGroupKeys(validGroupKeys.slice(0, REDIS_LARGE_KEYSPACE_MAX_EXPANDED_GROUPS)); return; } setExpandedGroupKeys(validGroupKeys); }; const renderValueEditor = () => { if (!keyValue || !selectedKey) { return
选择一个 Key 查看详情
; } const renderStringValue = () => { const strValue = String(keyValue.value); // 根据查看模式生成显示内容 const getDisplayContent = () => { if (viewMode === 'hex') { return { displayValue: toHexDisplay(strValue), isBinary: true, encoding: 'HEX' }; } else if (viewMode === 'text') { return { displayValue: strValue, isBinary: false, encoding: 'Text' }; } else if (viewMode === 'utf8') { try { const bytes = new Uint8Array(strValue.length); for (let i = 0; i < strValue.length; i++) { bytes[i] = strValue.charCodeAt(i) & 0xFF; } const decoded = new TextDecoder('utf-8', { fatal: false }).decode(bytes); return { displayValue: decoded, isBinary: false, encoding: 'UTF-8' }; } catch (e) { return { displayValue: strValue, isBinary: false, encoding: 'UTF-8 (失败)' }; } } else { // auto mode const { displayValue, isBinary, isJson, encoding } = formatStringValue(strValue); return { displayValue, isBinary, encoding }; } }; const { displayValue, isBinary, encoding } = getDisplayContent(); const isJson = viewMode === 'auto' && formatStringValue(strValue).isJson; return (
{encoding && `编码: ${encoding}`} setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{!isBinary && viewMode === 'auto' && ( )} {(isBinary || viewMode !== 'auto') && ( {viewMode !== 'auto' ? '切换到"自动"模式以编辑' : '二进制数据不支持编辑'} )}
); }; const renderHashValue = () => { // 根据查看模式处理值 const processValue = (value: string) => { if (viewMode === 'hex') { return { displayValue: toHexDisplay(value), isBinary: true, isJson: false, encoding: 'HEX' }; } else if (viewMode === 'text') { return { displayValue: value, isBinary: false, isJson: false, encoding: 'Text' }; } else if (viewMode === 'utf8') { try { const bytes = new Uint8Array(value.length); for (let i = 0; i < value.length; i++) { bytes[i] = value.charCodeAt(i) & 0xFF; } const decoded = new TextDecoder('utf-8', { fatal: false }).decode(bytes); return { displayValue: decoded, isBinary: false, isJson: false, encoding: 'UTF-8' }; } catch (e) { return { displayValue: value, isBinary: false, isJson: false, encoding: 'UTF-8 (失败)' }; } } else { // auto mode return formatStringValue(value); } }; const data = Object.entries(keyValue.value as Record).map(([field, value]) => { const { displayValue, isBinary, isJson, encoding } = processValue(value); return { field, value, displayValue, isBinary, isJson, encoding }; }); const handleEditHashField = async (field: string, newValue: string) => { const config = getConfig(); if (!config) return; try { const res = await (window as any).go.app.App.RedisSetHashField(config, selectedKey, field, newValue); if (res.success) { message.success('修改成功'); loadKeyValue(selectedKey); } else { message.error('修改失败: ' + res.message); } } catch (e: any) { message.error('修改失败: ' + (e?.message || String(e))); } }; const handleDeleteHashField = async (field: string) => { const config = getConfig(); if (!config) return; try { const res = await (window as any).go.app.App.RedisDeleteHashField(config, selectedKey, field); if (res.success) { message.success('删除成功'); loadKeyValue(selectedKey); } else { message.error('删除失败: ' + res.message); } } catch (e: any) { message.error('删除失败: ' + (e?.message || String(e))); } }; return (
setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{ const tooltipContent = record.encoding && record.encoding !== 'UTF-8' ? `[${record.encoding}]\n${text}` : text; return ( {tooltipContent}} styles={{ root: { maxWidth: 600 } }}> {text} ); } }, { title: '操作', key: 'action', width: 120, render: (_: any, record: any) => ( setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{ const tooltipContent = record.encoding && record.encoding !== 'UTF-8' ? `[${record.encoding}]\n${text}` : text; return ( {tooltipContent}} styles={{ root: { maxWidth: 600 } }}> {text} ); } }, { title: '操作', key: 'action', width: 80, render: (_: any, record: any) => ( setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{ const tooltipContent = record.encoding && record.encoding !== 'UTF-8' ? `[${record.encoding}]\n${text}` : text; return ( {tooltipContent}} styles={{ root: { maxWidth: 600 } }}> {text} ); } }, { title: '操作', key: 'action', width: 80, render: (_: any, record: any) => ( setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{ const tooltipContent = record.encoding && record.encoding !== 'UTF-8' ? `[${record.encoding}]\n${text}` : text; return ( {tooltipContent}} styles={{ root: { maxWidth: 600 } }}> {text} ); } }, { title: '操作', key: 'action', width: 120, render: (_: any, record: any) => ( setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{ const tooltipContent = record.encoding && record.encoding !== 'UTF-8' ? `[${record.encoding}]\n${text}` : text; return ( {tooltipContent}} styles={{ root: { maxWidth: 720 } }}> {text} ); } }, { title: '操作', key: 'action', width: 140, render: (_: any, record: any) => (
{keyValue.type === 'string' && renderStringValue()} {keyValue.type === 'hash' && renderHashValue()} {keyValue.type === 'list' && renderListValue()} {keyValue.type === 'set' && renderSetValue()} {keyValue.type === 'zset' && renderZSetValue()} {keyValue.type === 'stream' && renderStreamValue()}
); }; if (!connection) { return
连接不存在
; } return (
{/* Left: Key List */}
} />
handleDeleteKeys(selectedKeys)} disabled={selectedKeys.length === 0} >
{isLargeKeyspace && (
已启用大数据量性能模式(简化节点渲染,最多保留 {REDIS_LARGE_KEYSPACE_MAX_EXPANDED_GROUPS} 个展开分组)
)}
handleTreeSelect(nodeKeys)} onCheck={(checked) => handleTreeCheck(checked)} style={{ padding: '8px 6px' }} />
{hasMore && (
)}
{/* Resizable Divider */} {/* Right: Value Viewer */}
{valueLoading ? (
加载中...
) : ( renderValueEditor() )}
{/* Edit String Modal */} setEditModalOpen(false)} width={800} styles={{ body: { height: 500 } }} > setEditValue(value || '')} options={{ minimap: { enabled: false }, lineNumbers: 'on', wordWrap: 'on', scrollBeyondLastLine: false, automaticLayout: true, folding: true }} /> {/* New Key Modal */} setNewKeyModalOpen(false)} >
{/* TTL Modal */} setTtlModalOpen(false)} >
{/* JSON Edit Modal with Monaco Editor */} { if (jsonEditConfig?.onSave) { await jsonEditConfig.onSave(jsonEditValueRef.current); } setJsonEditModalOpen(false); }} onCancel={() => setJsonEditModalOpen(false)} width={800} styles={{ body: { height: 500 } }} > { jsonEditValueRef.current = value || ''; }} onMount={(editor) => { jsonEditValueRef.current = jsonEditConfig?.value || ''; }} options={{ minimap: { enabled: false }, lineNumbers: 'on', wordWrap: 'on', scrollBeyondLastLine: false, automaticLayout: true, folding: true, formatOnPaste: true }} />
); }; export default RedisViewer;