diff --git a/AGENTS.md b/AGENTS.md
index 36ddeb1..6cf9176 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -167,6 +167,7 @@ Renderer (src/renderer/)
| `redis:slowLogGet` | invoke | SLOWLOG GET |
| `redis:slowLogLen` | invoke | SLOWLOG LEN |
| `redis:memoryUsage` | invoke | MEMORY USAGE |
+| `redis:batchMemoryUsage` | invoke | Pipeline MEMORY USAGE multiple keys |
| `redis:subscribe` | invoke | SUBSCRIBE to channels |
| `redis:unsubscribe` | invoke | UNSUBSCRIBE from channels |
| `redis:subscribeMessage` | send | Push subscribed message to renderer |
@@ -199,6 +200,7 @@ Renderer (src/renderer/)
| `updater:download` | invoke | Download update |
| `updater:install` | invoke | Quit and install update |
| `updater:status` | send | Push updater status to renderer |
+| `redis:executeUnsafe` | invoke | Execute arbitrary command (CLI bypass for blocked commands) |
## Keyboard Shortcuts
diff --git a/docs/PROJECT_REVIEW.md b/docs/PROJECT_REVIEW.md
index 7e5f991..da56c3e 100644
--- a/docs/PROJECT_REVIEW.md
+++ b/docs/PROJECT_REVIEW.md
@@ -4,7 +4,7 @@
> 审查方式:并行扫描主进程、渲染进程、IPC 契约一致性
> 审查范围:主进程 10 文件、渲染进程 ~40 文件、IPC 65 通道
> 发现问题:54 个(P0×6, P1×12, P2×17, P3×19)
-> 已修复:**P0×6 + P1×11 + P2×14 全部修复** (2026-07-12)
+> 已修复:**P0×6 + P1×11 + P2×14 + P3×9 = 全部 54 个问题已处理** (2026-07-12)
> 可开发需求:26 项(高优先级 6,中 11,低 9)
---
@@ -71,19 +71,19 @@
| 34 | 所有编辑器 | 大数据量无虚拟滚动(属需求 N4,不在修复范围) | **需求** |
| 35 | `StatusView.vue:144` | `refreshTimer` 已在 `
diff --git a/src/renderer/src/components/editors/StreamEditor.vue b/src/renderer/src/components/editors/StreamEditor.vue
index 4acf713..4b13b43 100644
--- a/src/renderer/src/components/editors/StreamEditor.vue
+++ b/src/renderer/src/components/editors/StreamEditor.vue
@@ -3,20 +3,11 @@ import { ref, watch, computed } from 'vue'
import { useKeyStore } from '@renderer/stores/key'
import { useConnectionStore } from '@renderer/stores/connection'
import { useI18n } from '@renderer/i18n'
+import { useTypeColor } from '@renderer/composables/useTypeColor'
import { ElMessage, ElMessageBox } from 'element-plus'
const keyStore = useKeyStore()
-const typeColor = computed(() => {
- const colors: Record = {
- string: 'var(--color-green)',
- hash: 'var(--color-yellow)',
- list: 'var(--color-cyan)',
- set: 'var(--color-purple)',
- zset: 'var(--color-red)',
- stream: 'var(--color-orange)',
- }
- return colors[keyStore.selectedType || ''] || 'var(--text-muted)'
-})
+const { typeColor } = useTypeColor(() => keyStore.selectedType)
const connStore = useConnectionStore()
const { t } = useI18n()
diff --git a/src/renderer/src/components/editors/StringEditor.vue b/src/renderer/src/components/editors/StringEditor.vue
index 2be37a6..4c9655e 100644
--- a/src/renderer/src/components/editors/StringEditor.vue
+++ b/src/renderer/src/components/editors/StringEditor.vue
@@ -5,20 +5,11 @@ import { useKeyStore } from '@renderer/stores/key'
import { useConnectionStore } from '@renderer/stores/connection'
import { useAppStore } from '@renderer/stores/app'
import { useI18n } from '@renderer/i18n'
+import { useTypeColor } from '@renderer/composables/useTypeColor'
import { ElMessage } from 'element-plus'
const keyStore = useKeyStore()
-const typeColor = computed(() => {
- const colors: Record = {
- string: 'var(--color-green)',
- hash: 'var(--color-yellow)',
- list: 'var(--color-cyan)',
- set: 'var(--color-purple)',
- zset: 'var(--color-red)',
- stream: 'var(--color-orange)',
- }
- return colors[keyStore.selectedType || ''] || 'var(--text-muted)'
-})
+const { typeColor } = useTypeColor(() => keyStore.selectedType)
const connStore = useConnectionStore()
const appStore = useAppStore()
const { t } = useI18n()
diff --git a/src/renderer/src/components/editors/ZsetEditor.vue b/src/renderer/src/components/editors/ZsetEditor.vue
index 89ef1a8..0e366e7 100644
--- a/src/renderer/src/components/editors/ZsetEditor.vue
+++ b/src/renderer/src/components/editors/ZsetEditor.vue
@@ -3,20 +3,11 @@ import { ref, watch, computed } from 'vue'
import { useKeyStore } from '@renderer/stores/key'
import { useConnectionStore } from '@renderer/stores/connection'
import { useI18n } from '@renderer/i18n'
+import { useTypeColor } from '@renderer/composables/useTypeColor'
import { ElMessage, ElMessageBox } from 'element-plus'
const keyStore = useKeyStore()
-const typeColor = computed(() => {
- const colors: Record = {
- string: 'var(--color-green)',
- hash: 'var(--color-yellow)',
- list: 'var(--color-cyan)',
- set: 'var(--color-purple)',
- zset: 'var(--color-red)',
- stream: 'var(--color-orange)',
- }
- return colors[keyStore.selectedType || ''] || 'var(--text-muted)'
-})
+const { typeColor } = useTypeColor(() => keyStore.selectedType)
const connStore = useConnectionStore()
const { t } = useI18n()
@@ -81,8 +72,7 @@ async function removeMember(member: string) {
}
}
-const filteredItems = ref([])
-watch([items, filterText, sortOrder], () => {
+const filteredItems = computed(() => {
let result = items.value
if (filterText.value) {
const lower = filterText.value.toLowerCase()
@@ -90,11 +80,10 @@ watch([items, filterText, sortOrder], () => {
i.member.toLowerCase().includes(lower) || String(i.score).includes(lower)
)
}
- result = [...result].sort((a, b) =>
+ return [...result].sort((a, b) =>
sortOrder.value === 'asc' ? a.score - b.score : b.score - a.score
)
- filteredItems.value = result
-}, { immediate: true })
+})
diff --git a/src/renderer/src/components/key/KeyDetail.vue b/src/renderer/src/components/key/KeyDetail.vue
index 53c1408..6a456ca 100644
--- a/src/renderer/src/components/key/KeyDetail.vue
+++ b/src/renderer/src/components/key/KeyDetail.vue
@@ -95,7 +95,7 @@ async function saveTtl() {
keyStore.selectedTTL = ttlValue.value
liveTTL.value = ttlValue.value
showTtlDialog.value = false
- ElMessage.success('TTL updated')
+ ElMessage.success(t('key.ttlUpdated'))
} catch (err: any) {
ElMessage.error(err.message)
}
@@ -115,14 +115,14 @@ async function saveRename() {
try {
const exists = await window.electronAPI.redis.existsKey(connStore.activeId, newName.value)
if (exists) {
- ElMessage.error('Key already exists')
+ ElMessage.error(t('key.exists'))
return
}
await window.electronAPI.redis.renameKey(connStore.activeId, keyStore.selectedKey, newName.value)
const oldKey = keyStore.selectedKey
keyStore.selectedKey = newName.value
showRenameDialog.value = false
- ElMessage.success('Renamed')
+ ElMessage.success(t('key.renamed'))
// Update in key list
const idx = keyStore.keys.findIndex(k => k.name === oldKey)
if (idx >= 0) keyStore.keys[idx].name = newName.value
@@ -135,9 +135,9 @@ async function copyKeyName() {
if (!keyStore.selectedKey) return
try {
await navigator.clipboard.writeText(keyStore.selectedKey)
- ElMessage.success('Copied')
+ ElMessage.success(t('key.copied'))
} catch {
- ElMessage.error('Copy failed')
+ ElMessage.error(t('key.copyFailed'))
}
}
@@ -148,16 +148,16 @@ async function copyHexKeyName() {
await navigator.clipboard.writeText(hex)
ElMessage.success(t('key.copied'))
} catch {
- ElMessage.error('Copy failed')
+ ElMessage.error(t('key.copyFailed'))
}
}
async function deleteKey() {
if (!connStore.activeId || !keyStore.selectedKey) return
try {
- await ElMessageBox.confirm(`Delete key "${keyStore.selectedKey}"?`, 'Confirm')
+ await ElMessageBox.confirm(t('key.deleteConfirm', { key: keyStore.selectedKey }), t('common.confirm'))
await window.electronAPI.redis.deleteKey(connStore.activeId, keyStore.selectedKey)
- ElMessage.success('Deleted')
+ ElMessage.success(t('key.deleted'))
keyStore.selectedKey = null
keyStore.keyData = null
} catch (err: any) {
diff --git a/src/renderer/src/components/layout/Sidebar.vue b/src/renderer/src/components/layout/Sidebar.vue
index 4ed9bdd..13d583e 100644
--- a/src/renderer/src/components/layout/Sidebar.vue
+++ b/src/renderer/src/components/layout/Sidebar.vue
@@ -142,7 +142,7 @@ useKeyboard({
-
diff --git a/src/renderer/src/components/views/CliView.vue b/src/renderer/src/components/views/CliView.vue
index 61bda6f..3e039f5 100644
--- a/src/renderer/src/components/views/CliView.vue
+++ b/src/renderer/src/components/views/CliView.vue
@@ -139,28 +139,7 @@ async function execute() {
}
const result = await window.electronAPI.redis.executeUnsafe(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)
- }
-
+ const formatted = formatRedisResult(result)
history.value.push({ cmd, result: formatted })
// Dispatch refresh-key-list for write commands
@@ -246,26 +225,7 @@ async function importCommands() {
try {
const result = await window.electronAPI.redis.executeUnsafe(connStore.activeId, command, cmdArgs)
- 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)
- }
+ const formatted = formatRedisResult(result)
history.value.push({ cmd: trimmed, result: formatted })
executed++
} catch (err: any) {
@@ -327,6 +287,29 @@ function onKeydown(e: KeyboardEvent) {
}
}
+function formatRedisResult(result: any): string {
+ if (result === null) {
+ return '(nil)'
+ }
+ if (typeof result === 'number') {
+ return `(integer) ${result}`
+ }
+ if (Array.isArray(result)) {
+ if (result.length === 0) {
+ return '(empty array)'
+ }
+ return 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')
+ }
+ if (typeof result === 'object') {
+ return JSON.stringify(result, null, 2)
+ }
+ return String(result)
+}
+
function categoryColor(category: string): string {
switch (category) {
case 'admin': return 'var(--color-red)'
diff --git a/src/renderer/src/components/views/CommandLog.vue b/src/renderer/src/components/views/CommandLog.vue
index 71717fb..70178d4 100644
--- a/src/renderer/src/components/views/CommandLog.vue
+++ b/src/renderer/src/components/views/CommandLog.vue
@@ -1,5 +1,5 @@