feat: ReJson/TairJson support

- string.ts: rejsonGet, rejsonSet, rejsonDel using JSON.GET/SET/DEL
- IPC + preload for rejsonGet/rejsonSet
- New ReJsonEditor.vue: Monaco JSON editor with JSON.GET/SET
- KeyDetail: dispatch ReJSON-RL/json/tair-json types to ReJsonEditor
This commit is contained in:
2026-07-07 21:58:17 +08:00
parent 1511427e62
commit b85e67541b
7 changed files with 128 additions and 2 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
- [x] **多格式查看器** — Text / Hex / Json / Binary / Msgpack / PHP Serialize / Java Serialize / Pickle / Brotli / Gzip / Deflate / DeflateRaw / Protobuf13 种格式) - [x] **多格式查看器** — Text / Hex / Json / Binary / Msgpack / PHP Serialize / Java Serialize / Pickle / Brotli / Gzip / Deflate / DeflateRaw / Protobuf13 种格式)
- [x] **自动格式检测** — 检查内容 magic bytes 自动选择查看器 - [x] **自动格式检测** — 检查内容 magic bytes 自动选择查看器
- [x] **自定义格式化器** — 外部可执行文件 + 模板参数 {KEY} {VALUE} {HEX} {HEX_FILE} - [x] **自定义格式化器** — 外部可执行文件 + 模板参数 {KEY} {VALUE} {HEX} {HEX_FILE}
- [ ] **ReJson / TairJson 支持** — JSON.GET / JSON.SET / JSON.DEL 编辑 - [x] **ReJson / TairJson 支持** — JSON.GET / JSON.SET / JSON.DEL 编辑
- [ ] **二进制 key 支持** — [Hex] 前缀输入,hex ↔ buffer 双向转换 - [ ] **二进制 key 支持** — [Hex] 前缀输入,hex ↔ buffer 双向转换
- [ ] **虚拟化滚动** — 大 key 集合(200k+)的性能优化,KeyList 树形视图虚拟化 - [ ] **虚拟化滚动** — 大 key 集合(200k+)的性能优化,KeyList 树形视图虚拟化
- [ ] **批量删除预览** — 先扫描预览,再确认删除,显示 key 数量和总大小 - [ ] **批量删除预览** — 先扫描预览,再确认删除,显示 key 数量和总大小
+8
View File
@@ -159,6 +159,14 @@ export function registerIpcHandlers(): void {
await redis.setStringValue(id, key, value) await redis.setStringValue(id, key, value)
})) }))
// ReJson
ipcMain.handle('redis:rejsonGet', async (_e, id: string, key: string, path: string) => {
return redis.rejsonGet(id, key, path)
})
ipcMain.handle('redis:rejsonSet', withLog('JSON.SET', async (_e, id: string, key: string, path: string, value: string) => {
await redis.rejsonSet(id, key, path, value)
}))
// Redis - Hash // Redis - Hash
ipcMain.handle('redis:hashScan', async (_e, id: string, key: string, cursor: string, count: number) => { ipcMain.handle('redis:hashScan', async (_e, id: string, key: string, cursor: string, count: number) => {
return redis.getHashFields(id, key, cursor, count) return redis.getHashFields(id, key, cursor, count)
+20 -1
View File
@@ -1,5 +1,5 @@
// src/main/redis/string.ts // src/main/redis/string.ts
// String 操作 // String + ReJson 操作
import { getClient } from './connection' import { getClient } from './connection'
export async function getStringValue(id: string, key: string): Promise<string | null> { export async function getStringValue(id: string, key: string): Promise<string | null> {
@@ -13,3 +13,22 @@ export async function setStringValue(id: string, key: string, value: string): Pr
if (!client) throw new Error(`Client ${id} not found`) if (!client) throw new Error(`Client ${id} not found`)
await client.set(key, value) await client.set(key, value)
} }
// ReJson operations
export async function rejsonGet(id: string, key: string, path: string = '.'): Promise<string> {
const client = getClient(id)
if (!client) throw new Error(`Client ${id} not found`)
return (client as any).call('JSON.GET', key, path)
}
export async function rejsonSet(id: string, key: string, path: string, value: string): Promise<void> {
const client = getClient(id)
if (!client) throw new Error(`Client ${id} not found`)
await (client as any).call('JSON.SET', key, path, value)
}
export async function rejsonDel(id: string, key: string, path: string = '.'): Promise<number> {
const client = getClient(id)
if (!client) throw new Error(`Client ${id} not found`)
return (client as any).call('JSON.DEL', key, path)
}
+2
View File
@@ -68,6 +68,8 @@ export interface ElectronAPI {
getKeyTTL: (id: string, key: string) => Promise<number> getKeyTTL: (id: string, key: string) => Promise<number>
getString: (id: string, key: string) => Promise<string | null> getString: (id: string, key: string) => Promise<string | null>
setString: (id: string, key: string, value: string) => Promise<void> setString: (id: string, key: string, value: string) => Promise<void>
rejsonGet: (id: string, key: string, path: string) => Promise<string>
rejsonSet: (id: string, key: string, path: string, value: string) => Promise<void>
deleteKey: (id: string, key: string) => Promise<void> deleteKey: (id: string, key: string) => Promise<void>
hashScan: (id: string, key: string, cursor: string, count: number) => hashScan: (id: string, key: string, cursor: string, count: number) =>
Promise<{ cursor: string; fields: [string, string][] }> Promise<{ cursor: string; fields: [string, string][] }>
+5
View File
@@ -43,6 +43,11 @@ const electronAPI = {
getString: (id: string, key: string): Promise<string | null> => ipcRenderer.invoke('redis:getString', id, key), getString: (id: string, key: string): Promise<string | null> => ipcRenderer.invoke('redis:getString', id, key),
setString: (id: string, key: string, value: string): Promise<void> => setString: (id: string, key: string, value: string): Promise<void> =>
ipcRenderer.invoke('redis:setString', id, key, value), ipcRenderer.invoke('redis:setString', id, key, value),
// ReJson
rejsonGet: (id: string, key: string, path: string): Promise<string> =>
ipcRenderer.invoke('redis:rejsonGet', id, key, path),
rejsonSet: (id: string, key: string, path: string, value: string): Promise<void> =>
ipcRenderer.invoke('redis:rejsonSet', id, key, path, value),
deleteKey: (id: string, key: string): Promise<void> => ipcRenderer.invoke('redis:deleteKey', id, key), deleteKey: (id: string, key: string): Promise<void> => ipcRenderer.invoke('redis:deleteKey', id, key),
hashScan: (id: string, key: string, cursor: string, count: number): Promise<any> => hashScan: (id: string, key: string, cursor: string, count: number): Promise<any> =>
ipcRenderer.invoke('redis:hashScan', id, key, cursor, count), ipcRenderer.invoke('redis:hashScan', id, key, cursor, count),
+2
View File
@@ -11,6 +11,7 @@ import ListEditor from './ListEditor.vue'
import SetEditor from './SetEditor.vue' import SetEditor from './SetEditor.vue'
import ZsetEditor from './ZsetEditor.vue' import ZsetEditor from './ZsetEditor.vue'
import StreamEditor from './StreamEditor.vue' import StreamEditor from './StreamEditor.vue'
import ReJsonEditor from './ReJsonEditor.vue'
const keyStore = useKeyStore() const keyStore = useKeyStore()
const connStore = useConnectionStore() const connStore = useConnectionStore()
@@ -148,6 +149,7 @@ async function refreshKey() {
<SetEditor v-else-if="keyStore.selectedType === 'set'" /> <SetEditor v-else-if="keyStore.selectedType === 'set'" />
<ZsetEditor v-else-if="keyStore.selectedType === 'zset'" /> <ZsetEditor v-else-if="keyStore.selectedType === 'zset'" />
<StreamEditor v-else-if="keyStore.selectedType === 'stream'" /> <StreamEditor v-else-if="keyStore.selectedType === 'stream'" />
<ReJsonEditor v-else-if="['ReJSON-RL', 'json', 'tair-json'].includes(keyStore.selectedType || '')" />
<div v-else class="unsupported"> <div v-else class="unsupported">
<el-icon><Warning /></el-icon> <el-icon><Warning /></el-icon>
<p>{{ t('key.unsupported', { type: keyStore.selectedType }) }}</p> <p>{{ t('key.unsupported', { type: keyStore.selectedType }) }}</p>
+90
View File
@@ -0,0 +1,90 @@
<script setup lang="ts">
// src/components/ReJsonEditor.vue
import { ref, watch, computed } from 'vue'
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { useKeyStore } from '@renderer/stores/key'
import { useConnectionStore } from '@renderer/stores/connection'
import { useAppStore } from '@renderer/stores/app'
import { ElMessage } from 'element-plus'
const keyStore = useKeyStore()
const connStore = useConnectionStore()
const appStore = useAppStore()
const editedValue = ref('')
const isEditing = ref(false)
const monacoTheme = computed(() => appStore.theme === 'light' ? 'vs' : 'vs-dark')
const editorOptions = computed(() => ({
readOnly: !isEditing.value,
minimap: { enabled: false },
fontSize: appStore.fontSize,
wordWrap: 'on' as const,
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
}))
async function loadValue() {
if (!connStore.activeId || !keyStore.selectedKey) return
try {
const raw = await window.electronAPI.redis.rejsonGet(connStore.activeId, keyStore.selectedKey, '.')
try {
editedValue.value = JSON.stringify(JSON.parse(raw), null, 2)
} catch {
editedValue.value = raw
}
} catch (err: any) {
editedValue.value = ''
}
}
watch(
() => keyStore.selectedKey,
() => { if (keyStore.selectedType === 'ReJSON-RL' || keyStore.selectedType === 'json' || keyStore.selectedType === 'tair-json') loadValue() },
{ immediate: true }
)
async function save() {
if (!connStore.activeId || !keyStore.selectedKey) return
try {
await window.electronAPI.redis.rejsonSet(connStore.activeId, keyStore.selectedKey, '.', editedValue.value)
isEditing.value = false
ElMessage.success('Saved')
} catch (err: any) {
ElMessage.error(err.message)
}
}
</script>
<template>
<div class="rejson-editor">
<div class="editor-toolbar">
<span class="type-badge">ReJSON</span>
<span class="spacer" />
<el-button v-if="!isEditing" size="small" type="primary" @click="isEditing = true">Edit</el-button>
<template v-else>
<el-button size="small" type="primary" @click="save">Save</el-button>
<el-button size="small" @click="isEditing = false; loadValue()">Cancel</el-button>
</template>
</div>
<div class="editor-content">
<vue-monaco-editor
v-model:value="editedValue"
theme="vs-dark"
language="json"
:options="editorOptions"
height="100%"
/>
</div>
</div>
</template>
<style scoped>
.rejson-editor { height: 100%; display: flex; flex-direction: column; }
.editor-toolbar { padding: 12px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--border-color); flex-shrink: 0; }
.type-badge { font-size: 10px; font-weight: 700; color: var(--accent-light); background: var(--accent-bg); padding: 2px 8px; border-radius: 4px; }
.spacer { flex: 1; }
.editor-content { flex: 1; overflow: hidden; }
</style>