style: optimize DB selector dropdown

- Replace el-select with custom el-dropdown for compact tab bar fit
- Trigger: compact pill with DB name + key count badge + arrow
- Dropdown: two-column layout (DB index + custom name | key count badge + edit icon)
- Edit icon hidden until hover, colored on hover
- Selected DB highlighted with accent color
- Remove duplicate style block
- Key counts formatted (10k+ → '10k')
This commit is contained in:
2026-07-07 23:49:47 +08:00
parent 3b039714f0
commit baa8917eae
+175 -85
View File
@@ -28,27 +28,27 @@ function loadCustomNames() {
} }
} }
function getDbLabel(dbIndex: number): string { function getDbShortLabel(dbIndex: number): string {
const count = dbKeyCounts.value[dbIndex]
const customName = customDbNames.value[String(dbIndex)] const customName = customDbNames.value[String(dbIndex)]
const baseLabel = count !== undefined && count > 0 return customName || `DB${dbIndex}`
? `DB${dbIndex} (${t('db.keyCount', { n: count })})` }
: count === 0
? `DB${dbIndex} (${t('db.noKeys')})` function getDbKeyCount(dbIndex: number): number | undefined {
: `DB${dbIndex}` return dbKeyCounts.value[dbIndex]
if (customName) {
return `${baseLabel} - ${customName}`
}
return baseLabel
} }
const dbList = computed(() => { const dbList = computed(() => {
return Array.from({ length: dbCount.value }, (_, i) => ({ return Array.from({ length: dbCount.value }, (_, i) => ({
value: i, value: i,
label: getDbLabel(i), label: getDbShortLabel(i),
count: getDbKeyCount(i),
customName: customDbNames.value[String(i)] || '',
})) }))
}) })
const currentLabel = computed(() => getDbShortLabel(selectedDb.value))
const currentCount = computed(() => getDbKeyCount(selectedDb.value))
async function handleEditName(dbIndex: number, event: MouseEvent) { async function handleEditName(dbIndex: number, event: MouseEvent) {
event.stopPropagation() event.stopPropagation()
const currentName = customDbNames.value[String(dbIndex)] || '' const currentName = customDbNames.value[String(dbIndex)] || ''
@@ -80,7 +80,6 @@ async function handleEditName(dbIndex: number, event: MouseEvent) {
async function fetchDbInfo() { async function fetchDbInfo() {
if (!connStore.activeId) return if (!connStore.activeId) return
try { try {
// Get actual DB count from CONFIG GET databases
const configResult = await window.electronAPI.redis.execute( const configResult = await window.electronAPI.redis.execute(
connStore.activeId, 'CONFIG', ['GET', 'databases'] connStore.activeId, 'CONFIG', ['GET', 'databases']
) )
@@ -92,7 +91,6 @@ async function fetchDbInfo() {
} }
try { try {
// Parse INFO keyspace for per-DB key counts
const info = await connStore.getInfo() const info = await connStore.getInfo()
const keyspace: Record<number, number> = {} const keyspace: Record<number, number> = {}
const match = info.match(/# Keyspace[\s\S]*?(?=\n#|$)/) const match = info.match(/# Keyspace[\s\S]*?(?=\n#|$)/)
@@ -105,7 +103,6 @@ async function fetchDbInfo() {
} }
} }
} }
// Mark DBs with 0 keys explicitly
for (let i = 0; i < dbCount.value; i++) { for (let i = 0; i < dbCount.value; i++) {
if (!(i in keyspace)) { if (!(i in keyspace)) {
keyspace[i] = 0 keyspace[i] = 0
@@ -142,31 +139,53 @@ async function switchDb(db: number) {
connStore.saveLastDb(connStore.activeId, db) connStore.saveLastDb(connStore.activeId, db)
await keyStore.selectDb(connStore.activeId, db) await keyStore.selectDb(connStore.activeId, db)
} }
function formatCount(n: number): string {
if (n >= 10000) return `${(n / 1000).toFixed(0)}k`
return String(n)
}
</script> </script>
<template> <template>
<div class="db-selector" v-if="connStore.isConnected"> <div class="db-selector" v-if="connStore.isConnected">
<span class="db-label">DB</span> <el-dropdown trigger="click" @command="switchDb" placement="bottom-start">
<el-select <div class="db-trigger">
:model-value="selectedDb" <span class="db-trigger-label">{{ currentLabel }}</span>
size="small" <span v-if="currentCount !== undefined && currentCount > 0" class="db-trigger-badge">
@change="switchDb" {{ formatCount(currentCount) }}
style="width: 160px" </span>
> <el-icon class="db-trigger-arrow"><ArrowDown /></el-icon>
<el-option </div>
v-for="db in dbList" <template #dropdown>
:key="db.value" <el-dropdown-menu class="db-dropdown">
:label="db.label" <div class="db-dropdown-header">
:value="db.value" <span>{{ t('db.label') }}</span>
> </div>
<div class="db-option-content"> <el-dropdown-item
<span class="db-option-label">{{ db.label }}</span> v-for="db in dbList"
<el-icon class="db-edit-icon" @click="(e: MouseEvent) => handleEditName(db.value, e)"> :key="db.value"
<EditPen /> :command="db.value"
</el-icon> :class="{ 'is-selected': db.value === selectedDb }"
</div> >
</el-option> <div class="db-option">
</el-select> <div class="db-option-left">
<span class="db-option-index">DB{{ db.value }}</span>
<span v-if="db.customName" class="db-option-name">{{ db.customName }}</span>
</div>
<div class="db-option-right">
<span v-if="db.count !== undefined && db.count > 0" class="db-option-count">
{{ formatCount(db.count) }}
</span>
<span v-else class="db-option-empty">{{ t('db.noKeys') }}</span>
<el-icon class="db-option-edit" @click="(e: MouseEvent) => handleEditName(db.value, e)">
<EditPen />
</el-icon>
</div>
</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div> </div>
</template> </template>
@@ -174,84 +193,155 @@ async function switchDb(db: number) {
.db-selector { .db-selector {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px;
padding: 0 8px;
} }
.db-label { /* Trigger button — compact, fits in tab bar */
.db-trigger {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
font-size: 12px;
color: var(--text-primary);
transition: all 0.15s;
white-space: nowrap;
user-select: none;
}
.db-trigger:hover {
border-color: var(--accent);
background: var(--bg-hover);
}
.db-trigger-label {
font-weight: 600;
font-family: 'Cascadia Code', 'Fira Code', monospace;
}
.db-trigger-badge {
font-size: 10px;
font-weight: 700;
color: var(--accent-light);
background: var(--accent-bg, rgba(99, 102, 241, 0.15));
padding: 1px 6px;
border-radius: 8px;
line-height: 1.4;
}
.db-trigger-arrow {
font-size: 10px; font-size: 10px;
color: var(--text-muted); color: var(--text-muted);
font-weight: 600; transition: transform 0.15s;
} }
.db-option-content { .db-trigger:hover .db-trigger-arrow {
color: var(--accent-light);
}
/* Dropdown */
:deep(.db-dropdown) {
min-width: 200px;
max-height: 360px;
overflow-y: auto;
}
.db-dropdown-header {
padding: 6px 12px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
border-bottom: 1px solid var(--border-color);
}
:deep(.el-dropdown-menu__item) {
padding: 0;
}
:deep(.el-dropdown-menu__item.is-selected) {
background: var(--bg-card-active);
color: var(--accent-light);
}
.db-option {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
padding: 6px 12px;
gap: 8px;
} }
.db-option-label { .db-option-left {
flex: 1; display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.db-option-index {
font-size: 12px;
font-weight: 600;
font-family: 'Cascadia Code', 'Fira Code', monospace;
color: var(--text-primary);
flex-shrink: 0;
}
.db-option-name {
font-size: 11px;
color: var(--accent-light);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.db-edit-icon { .db-option-right {
font-size: 12px; display: flex;
cursor: pointer; align-items: center;
opacity: 0.4; gap: 6px;
transition: opacity 0.2s;
margin-left: 4px;
flex-shrink: 0; flex-shrink: 0;
} }
.db-edit-icon:hover { .db-option-count {
opacity: 1; font-size: 10px;
color: var(--accent); font-weight: 600;
color: var(--accent-light);
background: var(--accent-bg, rgba(99, 102, 241, 0.15));
padding: 1px 6px;
border-radius: 8px;
} }
:deep(.el-select .el-input__wrapper) { .db-option-empty {
background: var(--bg-card);
border-color: var(--border-color);
box-shadow: none;
}
:deep(.el-select .el-input__wrapper:hover) {
border-color: var(--border-accent);
}
:deep(.el-select .el-input__wrapper.is-focus) {
border-color: var(--accent);
}
</style>
<style scoped>
.db-selector {
display: flex;
align-items: center;
gap: 4px;
padding: 0 8px;
}
.db-label {
font-size: 10px; font-size: 10px;
color: var(--text-muted); color: var(--text-muted);
font-weight: 600;
} }
:deep(.el-select .el-input__wrapper) { .db-option-edit {
background: var(--bg-card); font-size: 11px;
border-color: var(--border-color); cursor: pointer;
box-shadow: none; opacity: 0;
color: var(--text-muted);
transition: opacity 0.15s, color 0.15s;
padding: 2px;
border-radius: 3px;
} }
:deep(.el-select .el-input__wrapper:hover) { .el-dropdown-menu__item:hover .db-option-edit {
border-color: var(--border-accent); opacity: 0.6;
} }
:deep(.el-select .el-input__wrapper.is-focus) { .db-option-edit:hover {
border-color: var(--accent); opacity: 1 !important;
color: var(--accent);
background: var(--bg-hover);
}
:deep(.is-selected) .db-option-index {
color: var(--accent-light);
} }
</style> </style>