feat: Command Log dialog

- New commandLogger.ts: in-memory log (max 5000 entries)
- withLog() wrapper in ipc-handlers.ts for all write commands
- CommandLog.vue: table with time/command/duration, write-only filter
- IPC: redis:getCommandLog, redis:clearCommandLog
- Ctrl+G shortcut from Sidebar
- i18n keys (zh-CN + en)
This commit is contained in:
2026-07-05 21:48:33 +08:00
parent 15869943e8
commit d131a2732d
10 changed files with 270 additions and 38 deletions
+128
View File
@@ -0,0 +1,128 @@
<script setup lang="ts">
// src/components/CommandLog.vue
import { ref, onMounted } from 'vue'
import { useI18n } from '@renderer/i18n'
const { t } = useI18n()
interface CommandEntry {
id: number
time: string
connection: string
command: string
duration: number
isWrite: boolean
}
const entries = ref<CommandEntry[]>([])
const onlyWrite = ref(false)
const visible = defineModel<boolean>('visible', { default: false })
const filteredEntries = () => {
if (onlyWrite.value) {
return entries.value.filter(e => e.isWrite)
}
return entries.value
}
function formatTime(iso: string): string {
const d = new Date(iso)
return d.toLocaleTimeString()
}
function formatDuration(ms: number): string {
if (ms < 1) return '<1ms'
if (ms < 1000) return ms + 'ms'
return (ms / 1000).toFixed(2) + 's'
}
async function refresh() {
try {
entries.value = await window.electronAPI.redis.getCommandLog()
} catch {
// ignore
}
}
async function clear() {
try {
await window.electronAPI.redis.clearCommandLog()
entries.value = []
} catch {
// ignore
}
}
defineExpose({ refresh })
</script>
<template>
<el-dialog
:model-value="visible"
:title="t('commandLog.title')"
width="700px"
top="5vh"
@update:model-value="visible = $event"
@opened="refresh"
>
<div class="cmdlog-toolbar">
<el-checkbox v-model="onlyWrite" size="small">{{ t('commandLog.onlyWrite') }}</el-checkbox>
<div class="cmdlog-actions">
<el-button size="small" @click="refresh">{{ t('common.refresh') }}</el-button>
<el-button size="small" type="danger" @click="clear">{{ t('commandLog.clear') }}</el-button>
</div>
</div>
<div class="cmdlog-table-wrap">
<el-table
:data="filteredEntries()"
size="small"
stripe
height="420"
highlight-current-row
>
<el-table-column prop="id" label="#" width="50" />
<el-table-column :label="t('commandLog.time')" width="90">
<template #default="{ row }">{{ formatTime(row.time) }}</template>
</el-table-column>
<el-table-column prop="command" :label="t('commandLog.command')" min-width="200" show-overflow-tooltip />
<el-table-column :label="t('commandLog.duration')" width="80" align="right">
<template #default="{ row }">
<span :style="{ color: row.duration > 100 ? 'var(--warning)' : 'var(--success)' }">
{{ formatDuration(row.duration) }}
</span>
</template>
</el-table-column>
</el-table>
<div v-if="entries.length === 0" class="cmdlog-empty">
{{ t('commandLog.empty') }}
</div>
</div>
</el-dialog>
</template>
<style scoped>
.cmdlog-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.cmdlog-actions {
display: flex;
gap: 8px;
}
.cmdlog-table-wrap {
min-height: 200px;
}
.cmdlog-empty {
display: flex;
align-items: center;
justify-content: center;
height: 150px;
color: var(--text-muted);
font-size: 13px;
}
</style>
+4
View File
@@ -3,6 +3,7 @@
import { ref } from 'vue'
import ConnectionList from './ConnectionList.vue'
import NewConnectionDialog from './NewConnectionDialog.vue'
import CommandLog from './CommandLog.vue'
import { useConnectionStore } from '@renderer/stores/connection'
import { useKeyStore } from '@renderer/stores/key'
import { useI18n } from '@renderer/i18n'
@@ -16,6 +17,7 @@ const { t } = useI18n()
const dialogVisible = ref(false)
const editingConnection = ref<ConnectionConfig | null>(null)
const collapsed = ref(false)
const commandLogVisible = ref(false)
function handleEdit(conn: ConnectionConfig | null) {
editingConnection.value = conn
@@ -53,6 +55,7 @@ function refreshAll() {
useKeyboard({
'ctrl+n': handleNew,
'ctrl+r': refreshAll,
'ctrl+g': () => { commandLogVisible.value = true },
'f5': refreshAll,
'delete': deleteSelectedKey,
'escape': deselectKey,
@@ -73,6 +76,7 @@ useKeyboard({
v-model:visible="dialogVisible"
:connection="editingConnection"
/>
<CommandLog v-model:visible="commandLogVisible" />
</aside>
</template>
+10
View File
@@ -156,6 +156,16 @@ export default {
sortDesc: 'Descending',
scanned: '{n} keys scanned',
},
commandLog: {
title: 'Command Log',
time: 'Time',
command: 'Command',
duration: 'Duration',
connection: 'Connection',
onlyWrite: 'Write Only',
clear: 'Clear',
empty: 'No commands recorded',
},
status: {
title: 'Status',
database: 'Database',
+10
View File
@@ -156,6 +156,16 @@ export default {
sortDesc: '降序',
scanned: '已扫描 {n} 个',
},
commandLog: {
title: '命令日志',
time: '时间',
command: '命令',
duration: '耗时',
connection: '连接',
onlyWrite: '仅写操作',
clear: '清空',
empty: '暂无命令记录',
},
status: {
title: '状态',
database: '数据库',