feat: Redis查询结果优化 — 复制按钮+JSON自动格式化+语义化列名

1. GET/HGET/TTL等单值命令: columns改为[key,value]替代[command,result]
2. DBSIZE等无key命令保持[command,result]格式
3. 前端添加复制按钮(Clipboard API), 2秒反馈
4. JSON值自动检测并格式化显示(whitespace-pre-wrap)
5. 结果区最大高度350px→500px
6. cost_ms字段兼容处理
This commit is contained in:
logan committed 2026-05-28 15:09:37 +08:00
1 parent 828a0597c5
commit 18430f3717
2 files changed
+77 -14

No files matched your search

+58 -9
View File
@@ -5,7 +5,7 @@ import {
StarOff, Download, Search, ArrowUpDown, Database, Table2,
ArrowUp, ArrowDown, Eye, Hash, PanelLeftClose, Columns3,
KeyRound, FolderOpen, Server, Zap, ClipboardPaste,
History, PanelRightClose, PanelRightOpen,
History, PanelRightClose, PanelRightOpen, Copy, Check,
} from 'lucide-react';
// Card components replaced by design system utility classes
import { useActionPermission } from '@/hooks/useAuth';
@@ -264,6 +264,46 @@ export default function QueriesPage() {
downloadCSV(`query_result_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.csv`, exportToCSV(result.columns, result.rows));
}, [result]);
/* ---- Copy Result ---- */
const [copied, setCopied] = useState(false);
const handleCopyResult = useCallback(() => {
if (!result) return;
let text = '';
if (result.rows?.length && result.columns?.length) {
// Format as readable text
const lines = result.rows.map((row: Record<string, unknown>) =>
result.columns.map((col: string) => {
const val = String(row[col] ?? '');
// Try to pretty-format JSON values
try {
const parsed = JSON.parse(val);
if (typeof parsed === 'object') return JSON.stringify(parsed, null, 2);
} catch { /* not JSON */ }
return val;
}).join('\t')
);
text = [result.columns.join('\t'), ...lines].join('\n');
} else {
text = JSON.stringify(result, null, 2);
}
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
}, [result]);
/* ---- Smart cell value formatter (detect JSON) ---- */
const formatCellValue = useCallback((val: unknown) => {
const str = String(val ?? '');
try {
const parsed = JSON.parse(str);
if (typeof parsed === 'object' && parsed !== null) {
return { text: JSON.stringify(parsed, null, 2), isJson: true };
}
} catch { /* not JSON */ }
return { text: str, isJson: false };
}, []);
/* ---- History ---- */
const toggleFavorite = useCallback((id: number) => {
setHistory(prev => {
@@ -439,21 +479,27 @@ export default function QueriesPage() {
查询结果
</span>
<div className="flex items-center gap-3 text-xs dg-text-secondary">
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{result.execution_time_ms}ms</span>
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{result.execution_time_ms || result.cost_ms || 0}ms</span>
<span>{result.rows?.length || 0} 行</span>
<span>{result.columns?.length || 0} 列</span>
{result.rows && result.rows.length > 0 && (
<button onClick={handleExportResult}
className="dg-btn dg-btn-ghost dg-btn-sm">
<Download className="w-3 h-3" />导出 CSV
</button>
<>
<button onClick={handleCopyResult}
className="dg-btn dg-btn-ghost dg-btn-sm gap-1">
{copied ? <><Check className="w-3 h-3 text-green-400" /><span className="text-green-400">已复制</span></> : <><Copy className="w-3 h-3" />复制</>}
</button>
<button onClick={handleExportResult}
className="dg-btn dg-btn-ghost dg-btn-sm">
<Download className="w-3 h-3" />导出 CSV
</button>
</>
)}
</div>
</div>
</div>
<div className="p-0">
{result.columns && result.columns.length > 0 ? (
<div ref={resultContainerRef} onScroll={handleResultScroll} className="overflow-auto max-h-[350px]">
<div ref={resultContainerRef} onScroll={handleResultScroll} className="overflow-auto max-h-[500px]">
<table className="w-full text-sm">
<thead className="sticky top-0 z-10">
<tr className="dg-table-header">
@@ -481,9 +527,12 @@ export default function QueriesPage() {
{result.columns.map(col => {
const isSensitive = sensitiveKeywords.some(k => col.toLowerCase().includes(k));
const cellValue = row[col] ?? '';
const { text: displayText, isJson } = formatCellValue(cellValue);
return (
<td key={col} className="px-3 py-2 text-xs max-w-[200px] truncate" title={String(cellValue)}>
{isSensitive ? <span className="text-yellow-400 italic">{maskData(String(cellValue))}</span> : String(cellValue)}
<td key={col} className={`px-3 py-2 text-xs ${isJson ? 'max-w-[400px] whitespace-pre-wrap break-all font-mono text-[11px] leading-relaxed' : 'max-w-[200px] truncate'}`} title={String(cellValue)}>
{isSensitive ? <span className="text-yellow-400 italic">{maskData(String(cellValue))}</span> :
isJson ? <span className="text-blue-300">{displayText}</span> :
String(cellValue)}
</td>
);
})}