feat: CLI MULTI/EXEC transaction support

- CliView: detect MULTI/EXEC/DISCARD commands
- MULTI enters transaction mode (tx> prompt, yellow)
- Commands in tx mode are queued (QUEUED + queue count)
- EXEC executes all queued commands, shows array results
- DISCARD cancels transaction
- WATCH/UNWATCH handled as non-queued tx commands
- i18n keys for tx messages (zh-CN + en)
This commit is contained in:
2026-07-07 21:11:23 +08:00
parent c7e47c4d5b
commit 00c11d4dfe
4 changed files with 73 additions and 3 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
- [x] **Monaco 编辑器** — 替换 StringEditor 的 textarea,支持 JSON 折叠/展开/语法高亮
- [x] **Redis 命令自动补全** — CLI 输入时智能提示(基于 commands.js 命令数据库)
- [x] **CLI SUBSCRIBE/MONITOR** — 实时消息流 + MONITOR 命令流 + 停止按钮
- [ ] **CLI MULTI/EXEC** — 事务模式支持
- [x] **CLI MULTI/EXEC** — 事务模式支持
- [ ] **Stream 消费者组** — XINFO GROUPS / XINFO CONSUMERS 展开表格
## P2 — 重要功能增强
+60 -2
View File
@@ -15,6 +15,8 @@ const outputRef = ref<HTMLDivElement>()
const suggestionIndex = ref(-1)
const inputRef = ref<HTMLInputElement>()
const streamingMode = ref<'none' | 'subscribe' | 'monitor'>('none')
const txMode = ref(false)
const txQueue = ref<string[]>([])
const showSuggestions = computed(() => {
return suggestions.value.length > 0 && !input.value.includes(' ')
@@ -48,6 +50,57 @@ async function execute() {
const args = parts.slice(1)
try {
// Handle MULTI
if (command === 'MULTI' && !txMode.value) {
const result = await window.electronAPI.redis.execute(connStore.activeId, command, args)
if (result === 'OK') {
txMode.value = true
txQueue.value = []
history.value.push({ cmd, result: t('cli.txStarted') })
} else {
history.value.push({ cmd, result: String(result), error: true })
}
return
}
// Handle EXEC (end transaction)
if (command === 'EXEC' && txMode.value) {
const result = await window.electronAPI.redis.execute(connStore.activeId, command, args)
let formatted = ''
if (Array.isArray(result)) {
formatted = result.map((item: any, i: number) => {
if (item === null) return `${i + 1}) (nil)`
if (typeof item === 'number') return `${i + 1}) (integer) ${item}`
if (item instanceof Error) return `${i + 1}) (error) ${item.message}`
return `${i + 1}) "${item}"`
}).join('\n')
} else {
formatted = String(result)
}
history.value.push({ cmd: `EXEC (${txQueue.value.length} commands)`, result: formatted })
txMode.value = false
txQueue.value = []
return
}
// Handle DISCARD (cancel transaction)
if (command === 'DISCARD' && txMode.value) {
await window.electronAPI.redis.execute(connStore.activeId, command, args)
txMode.value = false
txQueue.value = []
history.value.push({ cmd, result: t('cli.txDiscarded') })
return
}
// In transaction mode: queue command
if (txMode.value && !['MULTI', 'EXEC', 'DISCARD', 'WATCH', 'UNWATCH'].includes(command)) {
const result = await window.electronAPI.redis.execute(connStore.activeId, command, args)
txQueue.value.push(cmd)
const queueInfo = t('cli.txQueue', { n: txQueue.value.length })
history.value.push({ cmd, result: `${t('cli.txQueued')}${queueInfo}` })
return
}
// Handle SUBSCRIBE / PSUBSCRIBE
if (command === 'SUBSCRIBE' || command === 'PSUBSCRIBE') {
const channels = args.filter(a => !a.startsWith('"'))
@@ -232,13 +285,13 @@ onUnmounted(() => {
</div>
<div class="cli-input-bar">
<template v-if="streamingMode === 'none'">
<span class="prompt">&gt;</span>
<span class="prompt" :class="{ tx: txMode }">{{ txMode ? t('cli.txPrompt') : '>' }}</span>
<div class="cli-input-wrapper">
<input
ref="inputRef"
v-model="input"
class="cli-input"
:placeholder="t('cli.placeholder')"
:placeholder="txMode ? t('cli.txQueue', { n: txQueue.length }) : t('cli.placeholder')"
@keydown="onKeydown"
autofocus
/>
@@ -303,6 +356,11 @@ onUnmounted(() => {
margin-right: 6px;
}
.prompt.tx {
color: var(--warning);
font-weight: 700;
}
.cli-result {
color: var(--text-secondary);
margin: 4px 0 0 18px;
+6
View File
@@ -142,6 +142,12 @@ export default {
subscribed: 'Subscribed, waiting for messages...',
monitoring: 'Monitoring, waiting for commands...',
subscribedTo: 'Subscribed to: {channels}',
txStarted: 'Transaction started, commands are queued...',
txQueued: 'QUEUED',
txExecuted: 'Transaction executed',
txDiscarded: 'Transaction discarded',
txQueue: 'Queue ({n})',
txPrompt: 'tx>',
},
slowlog: {
title: 'Slow Log',
+6
View File
@@ -142,6 +142,12 @@ export default {
subscribed: '已订阅,等待消息...',
monitoring: '监控中,等待命令...',
subscribedTo: '已订阅频道: {channels}',
txStarted: '已开启事务,输入命令入队...',
txQueued: '已入队',
txExecuted: '事务已执行',
txDiscarded: '事务已取消',
txQueue: '队列 ({n})',
txPrompt: 'tx>',
},
slowlog: {
title: '慢日志',