feat(phase4): list/set/zset editors, CLI, slow log, DB selector
- ListEditor: pagination, push, edit, remove elements - SetEditor: filter, add, remove members - ZsetEditor: filter, add with score, remove members - CliView: interactive terminal with command history - SlowLogView: slow log table with time/duration/command - DbSelector: dropdown to switch Redis databases (0-15) - StatusBar integrated with DB selector - MainArea: added CLI and Slow Log tabs - KeyDetail: dispatch to List/Set/Zset editors - RedisService: list/set/zset/slowlog/memory operations - Preload: all new IPC channels typed
This commit is contained in:
@@ -121,4 +121,62 @@ export function registerIpcHandlers(): void {
|
||||
ipcMain.handle('redis:dbSize', async (_e, id: string) => {
|
||||
return redisService.dbSize(id)
|
||||
})
|
||||
|
||||
// List
|
||||
ipcMain.handle('redis:listRange', async (_e, id: string, key: string, start: number, stop: number) => {
|
||||
return redisService.listRange(id, key, start, stop)
|
||||
})
|
||||
ipcMain.handle('redis:listLength', async (_e, id: string, key: string) => {
|
||||
return redisService.listLength(id, key)
|
||||
})
|
||||
ipcMain.handle('redis:listPush', async (_e, id: string, key: string, values: string[]) => {
|
||||
return redisService.listPush(id, key, ...values)
|
||||
})
|
||||
ipcMain.handle('redis:listSet', async (_e, id: string, key: string, index: number, value: string) => {
|
||||
await redisService.listSet(id, key, index, value)
|
||||
})
|
||||
ipcMain.handle('redis:listRemove', async (_e, id: string, key: string, count: number, value: string) => {
|
||||
return redisService.listRemove(id, key, count, value)
|
||||
})
|
||||
|
||||
// Set
|
||||
ipcMain.handle('redis:setMembers', async (_e, id: string, key: string) => {
|
||||
return redisService.setMembers(id, key)
|
||||
})
|
||||
ipcMain.handle('redis:setSize', async (_e, id: string, key: string) => {
|
||||
return redisService.setSize(id, key)
|
||||
})
|
||||
ipcMain.handle('redis:setAdd', async (_e, id: string, key: string, members: string[]) => {
|
||||
return redisService.setAdd(id, key, ...members)
|
||||
})
|
||||
ipcMain.handle('redis:setRemove', async (_e, id: string, key: string, members: string[]) => {
|
||||
return redisService.setRemove(id, key, ...members)
|
||||
})
|
||||
|
||||
// Sorted set
|
||||
ipcMain.handle('redis:zsetRange', async (_e, id: string, key: string, start: number, stop: number, withScores: boolean) => {
|
||||
return redisService.zsetRange(id, key, start, stop, withScores)
|
||||
})
|
||||
ipcMain.handle('redis:zsetSize', async (_e, id: string, key: string) => {
|
||||
return redisService.zsetSize(id, key)
|
||||
})
|
||||
ipcMain.handle('redis:zsetAdd', async (_e, id: string, key: string, score: number, member: string) => {
|
||||
return redisService.zsetAdd(id, key, score, member)
|
||||
})
|
||||
ipcMain.handle('redis:zsetRemove', async (_e, id: string, key: string, members: string[]) => {
|
||||
return redisService.zsetRemove(id, key, ...members)
|
||||
})
|
||||
|
||||
// Slow log
|
||||
ipcMain.handle('redis:slowLogGet', async (_e, id: string, count: number) => {
|
||||
return redisService.slowLogGet(id, count)
|
||||
})
|
||||
ipcMain.handle('redis:slowLogLen', async (_e, id: string) => {
|
||||
return redisService.slowLogLen(id)
|
||||
})
|
||||
|
||||
// Memory
|
||||
ipcMain.handle('redis:memoryUsage', async (_e, id: string, key: string) => {
|
||||
return redisService.memoryUsage(id, key)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -176,3 +176,105 @@ export async function dbSize(id: string): Promise<number> {
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.dbsize() as Promise<number>
|
||||
}
|
||||
|
||||
// List operations
|
||||
export async function listRange(id: string, key: string, start: number, stop: number): Promise<string[]> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.lrange(key, start, stop)
|
||||
}
|
||||
|
||||
export async function listLength(id: string, key: string): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.llen(key)
|
||||
}
|
||||
|
||||
export async function listPush(id: string, key: string, ...values: string[]): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.rpush(key, ...values)
|
||||
}
|
||||
|
||||
export async function listSet(id: string, key: string, index: number, value: string): Promise<void> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
await client.lset(key, index, value)
|
||||
}
|
||||
|
||||
export async function listRemove(id: string, key: string, count: number, value: string): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.lrem(key, count, value)
|
||||
}
|
||||
|
||||
// Set operations
|
||||
export async function setMembers(id: string, key: string): Promise<string[]> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.smembers(key)
|
||||
}
|
||||
|
||||
export async function setSize(id: string, key: string): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.scard(key)
|
||||
}
|
||||
|
||||
export async function setAdd(id: string, key: string, ...members: string[]): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.sadd(key, ...members)
|
||||
}
|
||||
|
||||
export async function setRemove(id: string, key: string, ...members: string[]): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.srem(key, ...members)
|
||||
}
|
||||
|
||||
// Sorted set operations
|
||||
export async function zsetRange(id: string, key: string, start: number, stop: number, withScores: boolean): Promise<string[]> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
if (withScores) return client.zrange(key, start, stop, 'WITHSCORES')
|
||||
return client.zrange(key, start, stop)
|
||||
}
|
||||
|
||||
export async function zsetSize(id: string, key: string): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.zcard(key)
|
||||
}
|
||||
|
||||
export async function zsetAdd(id: string, key: string, score: number, member: string): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.zadd(key, score, member)
|
||||
}
|
||||
|
||||
export async function zsetRemove(id: string, key: string, ...members: string[]): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.zrem(key, ...members)
|
||||
}
|
||||
|
||||
// Slow log
|
||||
export async function slowLogGet(id: string, count: number): Promise<any[]> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.slowlog('GET', count) as Promise<any[]>
|
||||
}
|
||||
|
||||
export async function slowLogLen(id: string): Promise<number> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.slowlog('LEN') as Promise<number>
|
||||
}
|
||||
|
||||
// Memory info
|
||||
export async function memoryUsage(id: string, key: string): Promise<number | null> {
|
||||
const client = clients.get(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.memory('USAGE', key) as Promise<number | null>
|
||||
}
|
||||
|
||||
Vendored
+16
@@ -70,6 +70,22 @@ export interface ElectronAPI {
|
||||
setTTL: (id: string, key: string, ttl: number) => Promise<void>
|
||||
selectDb: (id: string, db: number) => Promise<void>
|
||||
dbSize: (id: string) => Promise<number>
|
||||
listRange: (id: string, key: string, start: number, stop: number) => Promise<string[]>
|
||||
listLength: (id: string, key: string) => Promise<number>
|
||||
listPush: (id: string, key: string, values: string[]) => Promise<number>
|
||||
listSet: (id: string, key: string, index: number, value: string) => Promise<void>
|
||||
listRemove: (id: string, key: string, count: number, value: string) => Promise<number>
|
||||
setMembers: (id: string, key: string) => Promise<string[]>
|
||||
setSize: (id: string, key: string) => Promise<number>
|
||||
setAdd: (id: string, key: string, members: string[]) => Promise<number>
|
||||
setRemove: (id: string, key: string, members: string[]) => Promise<number>
|
||||
zsetRange: (id: string, key: string, start: number, stop: number, withScores: boolean) => Promise<string[]>
|
||||
zsetSize: (id: string, key: string) => Promise<number>
|
||||
zsetAdd: (id: string, key: string, score: number, member: string) => Promise<number>
|
||||
zsetRemove: (id: string, key: string, members: string[]) => Promise<number>
|
||||
slowLogGet: (id: string, count: number) => Promise<any[]>
|
||||
slowLogLen: (id: string) => Promise<number>
|
||||
memoryUsage: (id: string, key: string) => Promise<number | null>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,43 @@ const electronAPI = {
|
||||
selectDb: (id: string, db: number): Promise<void> =>
|
||||
ipcRenderer.invoke('redis:selectDb', id, db),
|
||||
dbSize: (id: string): Promise<number> => ipcRenderer.invoke('redis:dbSize', id),
|
||||
// List
|
||||
listRange: (id: string, key: string, start: number, stop: number): Promise<string[]> =>
|
||||
ipcRenderer.invoke('redis:listRange', id, key, start, stop),
|
||||
listLength: (id: string, key: string): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:listLength', id, key),
|
||||
listPush: (id: string, key: string, values: string[]): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:listPush', id, key, values),
|
||||
listSet: (id: string, key: string, index: number, value: string): Promise<void> =>
|
||||
ipcRenderer.invoke('redis:listSet', id, key, index, value),
|
||||
listRemove: (id: string, key: string, count: number, value: string): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:listRemove', id, key, count, value),
|
||||
// Set
|
||||
setMembers: (id: string, key: string): Promise<string[]> =>
|
||||
ipcRenderer.invoke('redis:setMembers', id, key),
|
||||
setSize: (id: string, key: string): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:setSize', id, key),
|
||||
setAdd: (id: string, key: string, members: string[]): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:setAdd', id, key, members),
|
||||
setRemove: (id: string, key: string, members: string[]): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:setRemove', id, key, members),
|
||||
// Sorted set
|
||||
zsetRange: (id: string, key: string, start: number, stop: number, withScores: boolean): Promise<string[]> =>
|
||||
ipcRenderer.invoke('redis:zsetRange', id, key, start, stop, withScores),
|
||||
zsetSize: (id: string, key: string): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:zsetSize', id, key),
|
||||
zsetAdd: (id: string, key: string, score: number, member: string): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:zsetAdd', id, key, score, member),
|
||||
zsetRemove: (id: string, key: string, members: string[]): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:zsetRemove', id, key, members),
|
||||
// Slow log
|
||||
slowLogGet: (id: string, count: number): Promise<any[]> =>
|
||||
ipcRenderer.invoke('redis:slowLogGet', id, count),
|
||||
slowLogLen: (id: string): Promise<number> =>
|
||||
ipcRenderer.invoke('redis:slowLogLen', id),
|
||||
// Memory
|
||||
memoryUsage: (id: string, key: string): Promise<number | null> =>
|
||||
ipcRenderer.invoke('redis:memoryUsage', id, key),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/CliView.vue
|
||||
import { ref, nextTick, watch } from 'vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
const input = ref('')
|
||||
const history = ref<{ cmd: string; result: string; error?: boolean }[]>([])
|
||||
const historyIndex = ref(-1)
|
||||
const outputRef = ref<HTMLDivElement>()
|
||||
|
||||
async function execute() {
|
||||
if (!connStore.activeId || !input.value.trim()) return
|
||||
|
||||
const cmd = input.value.trim()
|
||||
input.value = ''
|
||||
historyIndex.value = -1
|
||||
|
||||
const parts = cmd.split(/\s+/)
|
||||
const command = parts[0].toUpperCase()
|
||||
const args = parts.slice(1)
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.redis.execute(connStore.activeId, command, args)
|
||||
let formatted = ''
|
||||
|
||||
if (result === null) {
|
||||
formatted = '(nil)'
|
||||
} else if (typeof result === 'number') {
|
||||
formatted = `(integer) ${result}`
|
||||
} else if (Array.isArray(result)) {
|
||||
if (result.length === 0) {
|
||||
formatted = '(empty array)'
|
||||
} else {
|
||||
formatted = result.map((item: any, i: number) => {
|
||||
if (item === null) return `${i + 1}) (nil)`
|
||||
if (typeof item === 'number') return `${i + 1}) (integer) ${item}`
|
||||
return `${i + 1}) "${item}"`
|
||||
}).join('\n')
|
||||
}
|
||||
} else if (typeof result === 'object') {
|
||||
formatted = JSON.stringify(result, null, 2)
|
||||
} else {
|
||||
formatted = String(result)
|
||||
}
|
||||
|
||||
history.value.push({ cmd, result: formatted })
|
||||
} catch (err: any) {
|
||||
history.value.push({ cmd, result: `ERROR: ${err.message}`, error: true })
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
if (outputRef.value) {
|
||||
outputRef.value.scrollTop = outputRef.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
execute()
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
if (historyIndex.value < history.value.length - 1) {
|
||||
historyIndex.value++
|
||||
input.value = history.value[history.value.length - 1 - historyIndex.value].cmd
|
||||
}
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
if (historyIndex.value > 0) {
|
||||
historyIndex.value--
|
||||
input.value = history.value[history.value.length - 1 - historyIndex.value].cmd
|
||||
} else {
|
||||
historyIndex.value = -1
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cli-view">
|
||||
<div class="cli-output" ref="outputRef">
|
||||
<div v-for="(item, index) in history" :key="index" class="cli-entry">
|
||||
<div class="cli-cmd">
|
||||
<span class="prompt">></span>
|
||||
<span>{{ item.cmd }}</span>
|
||||
</div>
|
||||
<pre class="cli-result" :class="{ error: item.error }">{{ item.result }}</pre>
|
||||
</div>
|
||||
<div v-if="history.length === 0" class="cli-welcome">
|
||||
Redis CLI - type commands below
|
||||
</div>
|
||||
</div>
|
||||
<div class="cli-input-bar">
|
||||
<span class="prompt">></span>
|
||||
<input
|
||||
v-model="input"
|
||||
class="cli-input"
|
||||
placeholder="Enter Redis command..."
|
||||
@keydown="onKeydown"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cli-view {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.cli-output {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cli-welcome {
|
||||
color: var(--text-muted);
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.cli-entry {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cli-cmd {
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: var(--color-green);
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.cli-result {
|
||||
color: var(--text-secondary);
|
||||
margin: 4px 0 0 18px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cli-result.error {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.cli-input-bar {
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cli-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
||||
font-size: 13px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.cli-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/DbSelector.vue
|
||||
import { ref, watch } from 'vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
|
||||
const connStore = useConnectionStore()
|
||||
const keyStore = useKeyStore()
|
||||
|
||||
const selectedDb = ref(0)
|
||||
const dbs = Array.from({ length: 16 }, (_, i) => i)
|
||||
|
||||
watch(
|
||||
() => connStore.isConnected,
|
||||
(connected) => {
|
||||
if (connected) {
|
||||
selectedDb.value = 0
|
||||
keyStore.currentDb = 0
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function switchDb(db: number) {
|
||||
if (!connStore.activeId) return
|
||||
selectedDb.value = db
|
||||
await keyStore.selectDb(connStore.activeId, db)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="db-selector" v-if="connStore.isConnected">
|
||||
<span class="db-label">DB</span>
|
||||
<el-select
|
||||
:model-value="selectedDb"
|
||||
size="small"
|
||||
@change="switchDb"
|
||||
style="width: 70px"
|
||||
>
|
||||
<el-option
|
||||
v-for="db in dbs"
|
||||
:key="db"
|
||||
:label="db"
|
||||
:value="db"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.db-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.db-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,9 @@ import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import StringEditor from './StringEditor.vue'
|
||||
import HashEditor from './HashEditor.vue'
|
||||
import ListEditor from './ListEditor.vue'
|
||||
import SetEditor from './SetEditor.vue'
|
||||
import ZsetEditor from './ZsetEditor.vue'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const connStore = useConnectionStore()
|
||||
@@ -70,6 +73,9 @@ async function refreshKey() {
|
||||
<div class="detail-content">
|
||||
<StringEditor v-if="keyStore.selectedType === 'string'" />
|
||||
<HashEditor v-else-if="keyStore.selectedType === 'hash'" />
|
||||
<ListEditor v-else-if="keyStore.selectedType === 'list'" />
|
||||
<SetEditor v-else-if="keyStore.selectedType === 'set'" />
|
||||
<ZsetEditor v-else-if="keyStore.selectedType === 'zset'" />
|
||||
<div v-else class="unsupported">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<p>Type "{{ keyStore.selectedType }}" viewer not yet implemented</p>
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/ListEditor.vue
|
||||
import { ref, watch } from 'vue'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
const items = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
const pageStart = ref(0)
|
||||
const pageSize = 50
|
||||
const totalCount = ref(0)
|
||||
const showAddDialog = ref(false)
|
||||
const newItemValue = ref('')
|
||||
const editingIndex = ref<number | null>(null)
|
||||
const editingValue = ref('')
|
||||
|
||||
async function loadList() {
|
||||
if (!connStore.activeId || !keyStore.selectedKey) return
|
||||
loading.value = true
|
||||
try {
|
||||
const [len, data] = await Promise.all([
|
||||
window.electronAPI.redis.listLength(connStore.activeId, keyStore.selectedKey),
|
||||
window.electronAPI.redis.listRange(connStore.activeId, keyStore.selectedKey, pageStart.value, pageStart.value + pageSize - 1),
|
||||
])
|
||||
totalCount.value = len
|
||||
items.value = data
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => keyStore.selectedKey,
|
||||
() => { if (keyStore.selectedType === 'list') { pageStart.value = 0; loadList() } },
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function addItem() {
|
||||
if (!connStore.activeId || !keyStore.selectedKey || !newItemValue.value) return
|
||||
try {
|
||||
await window.electronAPI.redis.listPush(connStore.activeId, keyStore.selectedKey, [newItemValue.value])
|
||||
showAddDialog.value = false
|
||||
newItemValue.value = ''
|
||||
await loadList()
|
||||
ElMessage.success('Added')
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(index: number) {
|
||||
editingIndex.value = index
|
||||
editingValue.value = items.value[index]
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!connStore.activeId || !keyStore.selectedKey || editingIndex.value === null) return
|
||||
try {
|
||||
await window.electronAPI.redis.listSet(connStore.activeId, keyStore.selectedKey, editingIndex.value, editingValue.value)
|
||||
items.value[editingIndex.value] = editingValue.value
|
||||
editingIndex.value = null
|
||||
ElMessage.success('Saved')
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeItem(item: string) {
|
||||
if (!connStore.activeId || !keyStore.selectedKey) return
|
||||
try {
|
||||
await ElMessageBox.confirm(`Remove this element?`, 'Confirm')
|
||||
await window.electronAPI.redis.listRemove(connStore.activeId, keyStore.selectedKey, 1, item)
|
||||
await loadList()
|
||||
ElMessage.success('Removed')
|
||||
} catch (err: any) {
|
||||
if (err !== 'cancel') ElMessage.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (pageStart.value === 0) return
|
||||
pageStart.value = Math.max(0, pageStart.value - pageSize)
|
||||
loadList()
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (pageStart.value + pageSize >= totalCount.value) return
|
||||
pageStart.value += pageSize
|
||||
loadList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list-editor">
|
||||
<div class="editor-toolbar">
|
||||
<span class="type-badge">LIST</span>
|
||||
<span class="item-count">{{ totalCount }} elements</span>
|
||||
<el-button size="small" type="primary" @click="showAddDialog = true">Push</el-button>
|
||||
<el-button size="small" @click="loadList" :loading="loading">Refresh</el-button>
|
||||
</div>
|
||||
|
||||
<div class="list-content">
|
||||
<div v-for="(item, index) in items" :key="index" class="list-item">
|
||||
<span class="item-index">{{ pageStart + index }}</span>
|
||||
<template v-if="editingIndex === index">
|
||||
<el-input v-model="editingValue" size="small" class="item-input" />
|
||||
<el-button size="small" type="primary" text @click="saveEdit">Save</el-button>
|
||||
<el-button size="small" @click="editingIndex = null">Cancel</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="item-value">{{ item }}</span>
|
||||
<div class="item-actions">
|
||||
<el-button size="small" text @click="startEdit(index)">Edit</el-button>
|
||||
<el-button size="small" type="danger" text @click="removeItem(item)">Del</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="items.length === 0 && !loading" class="empty">Empty list</div>
|
||||
</div>
|
||||
|
||||
<div class="list-pagination" v-if="totalCount > pageSize">
|
||||
<el-button size="small" :disabled="pageStart === 0" @click="prevPage">Prev</el-button>
|
||||
<span class="page-info">{{ pageStart + 1 }}-{{ Math.min(pageStart + pageSize, totalCount) }} / {{ totalCount }}</span>
|
||||
<el-button size="small" :disabled="pageStart + pageSize >= totalCount" @click="nextPage">Next</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showAddDialog" title="Push Element" width="400px">
|
||||
<el-form-item label="Value">
|
||||
<el-input v-model="newItemValue" type="textarea" :rows="4" />
|
||||
</el-form-item>
|
||||
<template #footer>
|
||||
<el-button @click="showAddDialog = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="addItem">Push</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.list-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;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.item-count {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.list-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.list-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.item-index {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
min-width: 30px;
|
||||
font-family: 'Cascadia Code', monospace;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-family: 'Cascadia Code', monospace;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.list-pagination {
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -4,10 +4,12 @@ import { ref, watch } from 'vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import StatusView from './StatusView.vue'
|
||||
import KeyBrowser from './KeyBrowser.vue'
|
||||
import CliView from './CliView.vue'
|
||||
import SlowLogView from './SlowLogView.vue'
|
||||
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
type Tab = 'status' | 'keys' | 'cli'
|
||||
type Tab = 'status' | 'keys' | 'cli' | 'slowlog'
|
||||
const activeTab = ref<Tab>('status')
|
||||
|
||||
watch(
|
||||
@@ -39,10 +41,28 @@ watch(
|
||||
<el-icon><Document /></el-icon>
|
||||
Keys
|
||||
</button>
|
||||
<button
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'cli' }"
|
||||
@click="activeTab = 'cli'"
|
||||
>
|
||||
<el-icon><Monitor /></el-icon>
|
||||
CLI
|
||||
</button>
|
||||
<button
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'slowlog' }"
|
||||
@click="activeTab = 'slowlog'"
|
||||
>
|
||||
<el-icon><Clock /></el-icon>
|
||||
Slow Log
|
||||
</button>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<StatusView v-if="activeTab === 'status'" />
|
||||
<KeyBrowser v-else-if="activeTab === 'keys'" />
|
||||
<CliView v-else-if="activeTab === 'cli'" />
|
||||
<SlowLogView v-else-if="activeTab === 'slowlog'" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/SetEditor.vue
|
||||
import { ref, watch } from 'vue'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
const members = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
const filterText = ref('')
|
||||
const showAddDialog = ref(false)
|
||||
const newMember = ref('')
|
||||
|
||||
async function loadMembers() {
|
||||
if (!connStore.activeId || !keyStore.selectedKey) return
|
||||
loading.value = true
|
||||
try {
|
||||
members.value = await window.electronAPI.redis.setMembers(connStore.activeId, keyStore.selectedKey)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => keyStore.selectedKey,
|
||||
() => { if (keyStore.selectedType === 'set') loadMembers() },
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function addMember() {
|
||||
if (!connStore.activeId || !keyStore.selectedKey || !newMember.value) return
|
||||
try {
|
||||
const added = await window.electronAPI.redis.setAdd(connStore.activeId, keyStore.selectedKey, [newMember.value])
|
||||
if (added > 0) {
|
||||
members.value.push(newMember.value)
|
||||
}
|
||||
showAddDialog.value = false
|
||||
newMember.value = ''
|
||||
ElMessage.success('Added')
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMember(member: string) {
|
||||
if (!connStore.activeId || !keyStore.selectedKey) return
|
||||
try {
|
||||
await ElMessageBox.confirm(`Remove member "${member}"?`, 'Confirm')
|
||||
await window.electronAPI.redis.setRemove(connStore.activeId, keyStore.selectedKey, [member])
|
||||
members.value = members.value.filter(m => m !== member)
|
||||
ElMessage.success('Removed')
|
||||
} catch (err: any) {
|
||||
if (err !== 'cancel') ElMessage.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
const filteredMembers = ref<string[]>([])
|
||||
watch([members, filterText], () => {
|
||||
filteredMembers.value = filterText.value
|
||||
? members.value.filter(m => m.includes(filterText.value))
|
||||
: members.value
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="set-editor">
|
||||
<div class="editor-toolbar">
|
||||
<span class="type-badge">SET</span>
|
||||
<span class="item-count">{{ members.length }} members</span>
|
||||
<el-input v-model="filterText" placeholder="Filter..." size="small" clearable style="width: 150px" />
|
||||
<el-button size="small" type="primary" @click="showAddDialog = true">Add</el-button>
|
||||
<el-button size="small" @click="loadMembers" :loading="loading">Refresh</el-button>
|
||||
</div>
|
||||
|
||||
<div class="set-content">
|
||||
<div v-for="member in filteredMembers" :key="member" class="set-item">
|
||||
<span class="item-value">{{ member }}</span>
|
||||
<el-button size="small" type="danger" text @click="removeMember(member)">Del</el-button>
|
||||
</div>
|
||||
<div v-if="filteredMembers.length === 0 && !loading" class="empty">Empty set</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showAddDialog" title="Add Member" width="400px">
|
||||
<el-form-item label="Member">
|
||||
<el-input v-model="newMember" />
|
||||
</el-form-item>
|
||||
<template #footer>
|
||||
<el-button @click="showAddDialog = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="addMember">Add</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.set-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;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.item-count {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.set-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.set-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.set-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.item-value {
|
||||
font-size: 12px;
|
||||
font-family: 'Cascadia Code', monospace;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/SlowLogView.vue
|
||||
import { ref } from 'vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
interface SlowLogEntry {
|
||||
id: number
|
||||
timestamp: number
|
||||
duration: number
|
||||
command: string
|
||||
}
|
||||
|
||||
const entries = ref<SlowLogEntry[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function refresh() {
|
||||
if (!connStore.activeId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const raw = await window.electronAPI.redis.slowLogGet(connStore.activeId, 50)
|
||||
entries.value = raw.map((item: any) => ({
|
||||
id: item[0],
|
||||
timestamp: item[1],
|
||||
duration: item[2],
|
||||
command: Array.isArray(item[3]) ? item[3].join(' ') : String(item[3]),
|
||||
}))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(us: number) {
|
||||
if (us < 1000) return `${us}μs`
|
||||
if (us < 1000000) return `${(us / 1000).toFixed(1)}ms`
|
||||
return `${(us / 1000000).toFixed(2)}s`
|
||||
}
|
||||
|
||||
function formatTime(ts: number) {
|
||||
return new Date(ts * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="slow-log">
|
||||
<div class="toolbar">
|
||||
<span class="title">Slow Log</span>
|
||||
<el-button size="small" @click="refresh" :loading="loading">Refresh</el-button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-table :data="entries" size="small" max-height="400" empty-text="No slow log entries">
|
||||
<el-table-column label="Time" width="180">
|
||||
<template #default="{ row }">{{ formatTime(row.timestamp) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Duration" width="100">
|
||||
<template #default="{ row }">{{ formatDuration(row.duration) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Command" min-width="300">
|
||||
<template #default="{ row }">
|
||||
<span class="cmd-text">{{ row.command }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slow-log {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cmd-text {
|
||||
font-family: 'Cascadia Code', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,8 +2,11 @@
|
||||
// src/renderer/src/components/StatusBar.vue
|
||||
import { computed } from 'vue'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import DbSelector from './DbSelector.vue'
|
||||
|
||||
const connStore = useConnectionStore()
|
||||
const keyStore = useKeyStore()
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (connStore.connecting) return 'Connecting...'
|
||||
@@ -28,6 +31,9 @@ const statusClass = computed(() => {
|
||||
<span class="status-dot" :class="statusClass" />
|
||||
{{ statusText }}
|
||||
</span>
|
||||
<span class="statusbar-center">
|
||||
<DbSelector v-if="connStore.isConnected" />
|
||||
</span>
|
||||
<span class="statusbar-right">JRedisDesktop v2.0</span>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -53,6 +59,12 @@ const statusClass = computed(() => {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.statusbar-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/ZsetEditor.vue
|
||||
import { ref, watch } from 'vue'
|
||||
import { useKeyStore } from '@renderer/stores/key'
|
||||
import { useConnectionStore } from '@renderer/stores/connection'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const keyStore = useKeyStore()
|
||||
const connStore = useConnectionStore()
|
||||
|
||||
interface ZsetItem {
|
||||
member: string
|
||||
score: number
|
||||
}
|
||||
|
||||
const items = ref<ZsetItem[]>([])
|
||||
const loading = ref(false)
|
||||
const filterText = ref('')
|
||||
const showAddDialog = ref(false)
|
||||
const newMember = ref('')
|
||||
const newScore = ref(0)
|
||||
|
||||
async function loadMembers() {
|
||||
if (!connStore.activeId || !keyStore.selectedKey) return
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await window.electronAPI.redis.zsetRange(connStore.activeId, keyStore.selectedKey, 0, -1, true)
|
||||
const result: ZsetItem[] = []
|
||||
for (let i = 0; i < data.length; i += 2) {
|
||||
result.push({ member: data[i], score: Number(data[i + 1]) })
|
||||
}
|
||||
items.value = result
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => keyStore.selectedKey,
|
||||
() => { if (keyStore.selectedType === 'zset') loadMembers() },
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function addMember() {
|
||||
if (!connStore.activeId || !keyStore.selectedKey || !newMember.value) return
|
||||
try {
|
||||
await window.electronAPI.redis.zsetAdd(connStore.activeId, keyStore.selectedKey, newScore.value, newMember.value)
|
||||
items.value.push({ member: newMember.value, score: newScore.value })
|
||||
items.value.sort((a, b) => a.score - b.score)
|
||||
showAddDialog.value = false
|
||||
newMember.value = ''
|
||||
newScore.value = 0
|
||||
ElMessage.success('Added')
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMember(member: string) {
|
||||
if (!connStore.activeId || !keyStore.selectedKey) return
|
||||
try {
|
||||
await ElMessageBox.confirm(`Remove member "${member}"?`, 'Confirm')
|
||||
await window.electronAPI.redis.zsetRemove(connStore.activeId, keyStore.selectedKey, [member])
|
||||
items.value = items.value.filter(i => i.member !== member)
|
||||
ElMessage.success('Removed')
|
||||
} catch (err: any) {
|
||||
if (err !== 'cancel') ElMessage.error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
const filteredItems = ref<ZsetItem[]>([])
|
||||
watch([items, filterText], () => {
|
||||
filteredItems.value = filterText.value
|
||||
? items.value.filter(i => i.member.includes(filterText.value))
|
||||
: items.value
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="zset-editor">
|
||||
<div class="editor-toolbar">
|
||||
<span class="type-badge">ZSET</span>
|
||||
<span class="item-count">{{ items.length }} members</span>
|
||||
<el-input v-model="filterText" placeholder="Filter..." size="small" clearable style="width: 150px" />
|
||||
<el-button size="small" type="primary" @click="showAddDialog = true">Add</el-button>
|
||||
<el-button size="small" @click="loadMembers" :loading="loading">Refresh</el-button>
|
||||
</div>
|
||||
|
||||
<div class="zset-content">
|
||||
<div v-for="item in filteredItems" :key="item.member" class="zset-item">
|
||||
<span class="item-score">{{ item.score }}</span>
|
||||
<span class="item-member">{{ item.member }}</span>
|
||||
<el-button size="small" type="danger" text @click="removeMember(item.member)">Del</el-button>
|
||||
</div>
|
||||
<div v-if="filteredItems.length === 0 && !loading" class="empty">Empty sorted set</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showAddDialog" title="Add Member" width="400px">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="Member">
|
||||
<el-input v-model="newMember" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Score">
|
||||
<el-input-number v-model="newScore" :precision="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAddDialog = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="addMember">Add</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.zset-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;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.item-count {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.zset-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.zset-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.zset-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.item-score {
|
||||
font-size: 12px;
|
||||
font-family: 'Cascadia Code', monospace;
|
||||
color: var(--accent-light);
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.item-member {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-family: 'Cascadia Code', monospace;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -135,6 +135,21 @@ export const useKeyStore = defineStore('key', () => {
|
||||
keyData.value = result.fields
|
||||
break
|
||||
}
|
||||
case 'list': {
|
||||
const len = await window.electronAPI.redis.listLength(connId, key)
|
||||
keyData.value = { length: len }
|
||||
break
|
||||
}
|
||||
case 'set': {
|
||||
const size = await window.electronAPI.redis.setSize(connId, key)
|
||||
keyData.value = { size }
|
||||
break
|
||||
}
|
||||
case 'zset': {
|
||||
const size = await window.electronAPI.redis.zsetSize(connId, key)
|
||||
keyData.value = { size }
|
||||
break
|
||||
}
|
||||
default:
|
||||
keyData.value = null
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user