feat(phase4e): batch delete with multi-select

- KeyList: multi-select mode with checkbox toggle
- Batch delete bar shows selected count + delete button
- RedisService: batchDelete using pipeline
- Preload: batchDelete IPC channel
This commit is contained in:
2026-07-04 18:09:01 +08:00
parent ff09f5dc03
commit 4c66263477
6 changed files with 120 additions and 6 deletions
+3
View File
@@ -179,4 +179,7 @@ export function registerIpcHandlers(): void {
ipcMain.handle('redis:memoryUsage', async (_e, id: string, key: string) => {
return redisService.memoryUsage(id, key)
})
ipcMain.handle('redis:batchDelete', async (_e, id: string, keys: string[]) => {
return redisService.batchDelete(id, keys)
})
}
+12
View File
@@ -278,3 +278,15 @@ export async function memoryUsage(id: string, key: string): Promise<number | nul
if (!client) throw new Error(`Client ${id} not found`)
return client.memory('USAGE', key) as Promise<number | null>
}
export async function batchDelete(id: string, keys: string[]): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
if (keys.length === 0) return 0
const pipeline = client.pipeline()
for (const key of keys) {
pipeline.del(key)
}
const results = await pipeline.exec()
return results?.filter(([err]) => !err).length ?? 0
}
+1
View File
@@ -86,6 +86,7 @@ export interface ElectronAPI {
slowLogGet: (id: string, count: number) => Promise<any[]>
slowLogLen: (id: string) => Promise<number>
memoryUsage: (id: string, key: string) => Promise<number | null>
batchDelete: (id: string, keys: string[]) => Promise<number>
}
}
+2
View File
@@ -92,6 +92,8 @@ const electronAPI = {
// Memory
memoryUsage: (id: string, key: string): Promise<number | null> =>
ipcRenderer.invoke('redis:memoryUsage', id, key),
batchDelete: (id: string, keys: string[]): Promise<number> =>
ipcRenderer.invoke('redis:batchDelete', id, keys),
},
}
+101 -5
View File
@@ -3,12 +3,15 @@
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 filterText = ref('')
const showTree = ref(false)
const multiSelect = ref(false)
const selectedKeys = ref<Set<string>>(new Set())
async function loadMore() {
if (!connStore.activeId) return
@@ -17,18 +20,43 @@ async function loadMore() {
async function refreshKeys() {
if (!connStore.activeId) return
selectedKeys.value.clear()
await keyStore.scanKeys(connStore.activeId, '0', true)
}
async function selectKey(keyName: string) {
if (!connStore.activeId) return
if (multiSelect.value) {
if (selectedKeys.value.has(keyName)) {
selectedKeys.value.delete(keyName)
} else {
selectedKeys.value.add(keyName)
}
return
}
keyStore.selectedKey = keyName
await keyStore.loadKeyData(connStore.activeId, keyName)
}
function onPatternChange() {
if (!connStore.activeId) return
keyStore.scanKeys(connStore.activeId, '0', true)
async function batchDelete() {
if (!connStore.activeId || selectedKeys.value.size === 0) return
const count = selectedKeys.value.size
try {
await ElMessageBox.confirm(`Delete ${count} key(s)?`, 'Confirm')
const deleted = await window.electronAPI.redis.batchDelete(connStore.activeId, Array.from(selectedKeys.value))
ElMessage.success(`Deleted ${deleted} key(s)`)
selectedKeys.value.clear()
await refreshKeys()
} catch (err: any) {
if (err !== 'cancel') ElMessage.error(err.message)
}
}
function toggleMultiSelect() {
multiSelect.value = !multiSelect.value
if (!multiSelect.value) {
selectedKeys.value.clear()
}
}
const filteredKeys = ref<string[]>([])
@@ -70,9 +98,23 @@ watch(filterText, (val) => {
>
<el-icon><List /></el-icon>
</el-button>
<el-button
size="small"
:type="multiSelect ? 'danger' : 'default'"
@click="toggleMultiSelect"
>
<el-icon><Check /></el-icon>
</el-button>
</el-button-group>
</div>
<div v-if="multiSelect && selectedKeys.size > 0" class="batch-bar">
<span class="batch-count">{{ selectedKeys.size }} selected</span>
<el-button size="small" type="danger" @click="batchDelete">
Delete Selected
</el-button>
</div>
<div class="key-content">
<!-- Flat list -->
<div v-if="!showTree" class="key-flat-list">
@@ -80,9 +122,19 @@ watch(filterText, (val) => {
v-for="keyName in filteredKeys"
:key="keyName"
class="key-item"
:class="{ active: keyStore.selectedKey === keyName }"
:class="{
active: keyStore.selectedKey === keyName && !multiSelect,
selected: selectedKeys.has(keyName),
}"
@click="selectKey(keyName)"
>
<input
v-if="multiSelect"
type="checkbox"
class="key-checkbox"
:checked="selectedKeys.has(keyName)"
@click.stop
/>
<el-icon class="key-icon"><Document /></el-icon>
<span class="key-name">{{ keyName }}</span>
</div>
@@ -90,7 +142,23 @@ watch(filterText, (val) => {
<!-- Tree view -->
<div v-else class="key-tree">
<div v-for="keyName in filteredKeys" :key="keyName" class="key-item" @click="selectKey(keyName)">
<div
v-for="keyName in filteredKeys"
:key="keyName"
class="key-item"
:class="{
active: keyStore.selectedKey === keyName && !multiSelect,
selected: selectedKeys.has(keyName),
}"
@click="selectKey(keyName)"
>
<input
v-if="multiSelect"
type="checkbox"
class="key-checkbox"
:checked="selectedKeys.has(keyName)"
@click.stop
/>
<el-icon class="key-icon"><Document /></el-icon>
<span class="key-name">{{ keyName }}</span>
</div>
@@ -133,6 +201,22 @@ watch(filterText, (val) => {
flex-shrink: 0;
}
.batch-bar {
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--accent-bg);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.batch-count {
font-size: 12px;
font-weight: 600;
color: var(--accent-light);
}
.key-content {
flex: 1;
overflow-y: auto;
@@ -160,6 +244,18 @@ watch(filterText, (val) => {
color: var(--accent-light);
}
.key-item.selected {
background: rgba(245, 108, 108, 0.08);
color: var(--color-red);
}
.key-checkbox {
flex-shrink: 0;
width: 14px;
height: 14px;
accent-color: var(--accent-light);
}
.key-icon {
flex-shrink: 0;
color: var(--text-muted);
File diff suppressed because one or more lines are too long