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:
1 parent
18430f3717
commit
03ff845485
2 files changed
+62
-17
No files matched your search
@@ -14,6 +14,8 @@ import (
|
|||||||
"sql-platform/internal/middleware"
|
"sql-platform/internal/middleware"
|
||||||
"strings"
|
"strings"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"encoding/json"
|
||||||
|
"bytes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /api/v1/instances/:id/detail/databases
|
// GET /api/v1/instances/:id/detail/databases
|
||||||
@@ -703,24 +705,33 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string
|
|||||||
columns = []string{"field", "value"}
|
columns = []string{"field", "value"}
|
||||||
} else {
|
} else {
|
||||||
// Single value result (GET, HGET, TYPE, TTL, etc.)
|
// Single value result (GET, HGET, TYPE, TTL, etc.)
|
||||||
// Try to extract key name from command
|
|
||||||
keyName := ""
|
keyName := ""
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
keyName = parts[1]
|
keyName = parts[1]
|
||||||
}
|
}
|
||||||
if keyName != "" {
|
rawValue := fmt.Sprintf("%v", v)
|
||||||
rows = append(rows, map[string]interface{}{
|
// Try to pretty-format JSON with Chinese characters preserved
|
||||||
"key": keyName,
|
var prettyJSON string
|
||||||
"value": fmt.Sprintf("%v", v),
|
var parsed interface{}
|
||||||
})
|
if json.Unmarshal([]byte(rawValue), &parsed) == nil {
|
||||||
columns = []string{"key", "value"}
|
switch parsed.(type) {
|
||||||
} else {
|
case map[string]interface{}, []interface{}:
|
||||||
rows = append(rows, map[string]interface{}{
|
var buf bytes.Buffer
|
||||||
"command": command,
|
enc := json.NewEncoder(&buf)
|
||||||
"result": fmt.Sprintf("%v", v),
|
enc.SetEscapeHTML(false)
|
||||||
})
|
enc.SetIndent("", " ")
|
||||||
columns = []string{"command", "result"}
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ interface QueryResult {
|
|||||||
rows: Record<string, unknown>[];
|
rows: Record<string, unknown>[];
|
||||||
affected_rows: number;
|
affected_rows: number;
|
||||||
execution_time_ms: number;
|
execution_time_ms: number;
|
||||||
|
cost_ms?: number;
|
||||||
|
raw_value?: boolean;
|
||||||
|
raw_key?: string;
|
||||||
|
raw_text?: string;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryHistoryItem {
|
interface QueryHistoryItem {
|
||||||
@@ -480,9 +485,15 @@ export default function QueriesPage() {
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-3 text-xs dg-text-secondary">
|
<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>
|
<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>
|
{result.raw_text !== undefined ? (
|
||||||
<span>{result.columns?.length || 0} 列</span>
|
<span>{result.raw_value ? 'JSON' : typeof result.raw_text === 'string' ? 'String' : 'Value'}</span>
|
||||||
{result.rows && result.rows.length > 0 && (
|
) : (
|
||||||
|
<>
|
||||||
|
<span>{result.rows?.length || 0} 行</span>
|
||||||
|
<span>{result.columns?.length || 0} 列</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(result.rows?.length > 0 || result.raw_text !== undefined) && (
|
||||||
<>
|
<>
|
||||||
<button onClick={handleCopyResult}
|
<button onClick={handleCopyResult}
|
||||||
className="dg-btn dg-btn-ghost dg-btn-sm gap-1">
|
className="dg-btn dg-btn-ghost dg-btn-sm gap-1">
|
||||||
@@ -498,7 +509,30 @@ export default function QueriesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-0">
|
<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]">
|
<div ref={resultContainerRef} onScroll={handleResultScroll} className="overflow-auto max-h-[500px]">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="sticky top-0 z-10">
|
<thead className="sticky top-0 z-10">
|
||||||
|
|||||||
Reference in new issue
Block a user