feat: Redis单值结果直接展示+JSON中文格式化+悬浮复制

1. GET/HGET/TTL等单值命令: 直接展示raw值, 不再套table
2. JSON自动检测+格式化(中文正常显示, 2空格缩进)
3. 非JSON值(如false/数字)直接文本展示
4. 代码块右上角悬浮复制按钮(hover显示)
5. 工具栏显示值类型(JSON/String)替代行列数
This commit is contained in:
logan committed 2026-05-28 15:20:22 +08:00
1 parent 18430f3717
commit 03ff845485
2 files changed
+60 -15

No files matched your search

+24 -13
View File
@@ -14,6 +14,8 @@ import (
"sql-platform/internal/middleware"
"strings"
"strconv"
"encoding/json"
"bytes"
)
// GET /api/v1/instances/:id/detail/databases
@@ -703,25 +705,34 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string
columns = []string{"field", "value"}
} else {
// Single value result (GET, HGET, TYPE, TTL, etc.)
// Try to extract key name from command
keyName := ""
if len(parts) > 1 {
keyName = parts[1]
}
if keyName != "" {
rows = append(rows, map[string]interface{}{
"key": keyName,
"value": fmt.Sprintf("%v", v),
})
columns = []string{"key", "value"}
} else {
rows = append(rows, map[string]interface{}{
"command": command,
"result": fmt.Sprintf("%v", v),
})
columns = []string{"command", "result"}
rawValue := fmt.Sprintf("%v", v)
// Try to pretty-format JSON with Chinese characters preserved
var prettyJSON string
var parsed interface{}
if json.Unmarshal([]byte(rawValue), &parsed) == nil {
switch parsed.(type) {
case map[string]interface{}, []interface{}:
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(parsed)
prettyJSON = strings.TrimRight(buf.String(), "\n")
}
}
return gin.H{
"columns": []string{},
"rows": []interface{}{},
"total": 0,
"raw_value": prettyJSON != "",
"raw_key": keyName,
"raw_text": rawValue,
}, nil
}
}
return gin.H{
+36 -2
View File
@@ -25,6 +25,11 @@ interface QueryResult {
rows: Record<string, unknown>[];
affected_rows: number;
execution_time_ms: number;
cost_ms?: number;
raw_value?: boolean;
raw_key?: string;
raw_text?: string;
error?: string;
}
interface QueryHistoryItem {
@@ -480,9 +485,15 @@ 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 || result.cost_ms || 0}ms</span>
{result.raw_text !== undefined ? (
<span>{result.raw_value ? 'JSON' : typeof result.raw_text === 'string' ? 'String' : 'Value'}</span>
) : (
<>
<span>{result.rows?.length || 0} 行</span>
<span>{result.columns?.length || 0} 列</span>
{result.rows && result.rows.length > 0 && (
</>
)}
{(result.rows?.length > 0 || result.raw_text !== undefined) && (
<>
<button onClick={handleCopyResult}
className="dg-btn dg-btn-ghost dg-btn-sm gap-1">
@@ -498,7 +509,30 @@ export default function QueriesPage() {
</div>
</div>
<div className="p-0">
{result.columns && result.columns.length > 0 ? (
{result.raw_text !== undefined ? (
/* Raw value display (Redis GET/HGET/TTL etc.) */
<div className="p-4">
{result.raw_key && (
<div className="text-xs text-[var(--dg-text-muted)] mb-2 font-mono">{result.raw_key}</div>
)}
<div className="relative group">
<pre className={`text-sm leading-relaxed overflow-auto max-h-[450px] p-3 rounded-lg font-mono ${
result.raw_value
? 'bg-[var(--dg-bg-primary)] text-blue-300 whitespace-pre-wrap break-all'
: 'bg-[var(--dg-bg-primary)] text-[var(--dg-text-primary)] whitespace-pre-wrap break-all'
}`}>{result.raw_value
? (() => { try { return JSON.stringify(JSON.parse(String(result.raw_text)), null, 2); } catch { return String(result.raw_text); } })()
: String(result.raw_text)
}</pre>
<button onClick={() => {
const text = result.raw_value ? (() => { try { return JSON.stringify(JSON.parse(String(result.raw_text)), null, 2); } catch { return String(result.raw_text); } })() : String(result.raw_text);
navigator.clipboard.writeText(text).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); });
}} className="absolute top-2 right-2 dg-btn dg-btn-ghost dg-btn-sm opacity-0 group-hover:opacity-100 transition-opacity">
{copied ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
</button>
</div>
</div>
) : result.columns && result.columns.length > 0 ? (
<div ref={resultContainerRef} onScroll={handleResultScroll} className="overflow-auto max-h-[500px]">
<table className="w-full text-sm">
<thead className="sticky top-0 z-10">