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:
+1
-1
@@ -29,7 +29,7 @@
|
||||
- [x] **多格式查看器** — Text / Hex / Json / Binary / Msgpack / PHP Serialize / Java Serialize / Pickle / Brotli / Gzip / Deflate / DeflateRaw / Protobuf(13 种格式)
|
||||
- [x] **自动格式检测** — 检查内容 magic bytes 自动选择查看器
|
||||
- [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 集合(200k+)的性能优化,KeyList 树形视图虚拟化
|
||||
- [ ] **批量删除预览** — 先扫描预览,再确认删除,显示 key 数量和总大小
|
||||
|
||||
@@ -159,6 +159,14 @@ export function registerIpcHandlers(): void {
|
||||
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
|
||||
ipcMain.handle('redis:hashScan', async (_e, id: string, key: string, cursor: string, count: number) => {
|
||||
return redis.getHashFields(id, key, cursor, count)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/main/redis/string.ts
|
||||
// String 操作
|
||||
// String + ReJson 操作
|
||||
import { getClient } from './connection'
|
||||
|
||||
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`)
|
||||
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)
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -68,6 +68,8 @@ export interface ElectronAPI {
|
||||
getKeyTTL: (id: string, key: string) => Promise<number>
|
||||
getString: (id: string, key: string) => Promise<string | null>
|
||||
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>
|
||||
hashScan: (id: string, key: string, cursor: string, count: number) =>
|
||||
Promise<{ cursor: string; fields: [string, string][] }>
|
||||
|
||||
@@ -43,6 +43,11 @@ const electronAPI = {
|
||||
getString: (id: string, key: string): Promise<string | null> => ipcRenderer.invoke('redis:getString', id, key),
|
||||
setString: (id: string, key: string, value: string): Promise<void> =>
|
||||
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),
|
||||
hashScan: (id: string, key: string, cursor: string, count: number): Promise<any> =>
|
||||
ipcRenderer.invoke('redis:hashScan', id, key, cursor, count),
|
||||
|
||||
@@ -11,6 +11,7 @@ import ListEditor from './ListEditor.vue'
|
||||
import SetEditor from './SetEditor.vue'
|
||||
import ZsetEditor from './ZsetEditor.vue'
|
||||
import StreamEditor from './StreamEditor.vue'
|
||||
import ReJsonEditor from './ReJsonEditor.vue'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const connStore = useConnectionStore()
|
||||
@@ -148,6 +149,7 @@ async function refreshKey() {
|
||||
<SetEditor v-else-if="keyStore.selectedType === 'set'" />
|
||||
<ZsetEditor v-else-if="keyStore.selectedType === 'zset'" />
|
||||
<StreamEditor v-else-if="keyStore.selectedType === 'stream'" />
|
||||
<ReJsonEditor v-else-if="['ReJSON-RL', 'json', 'tair-json'].includes(keyStore.selectedType || '')" />
|
||||
<div v-else class="unsupported">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<p>{{ t('key.unsupported', { type: keyStore.selectedType }) }}</p>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user