feat: P3 batch 1 - context menus, shortcuts, connection management
- Key list right-click context menu (copy/delete/export/memory/copy value) - Shift+Click range selection + Select All checkbox - Keyboard shortcuts: Ctrl+, (settings), Ctrl+S (save), Ctrl+L (clear CLI), Ctrl+/ (hotkeys help) - CLI auto-sync: SELECT updates DB, write commands refresh key list - Connection color marking (6 predefined colors) - Duplicate connection button - Connection drag-and-drop reorder (native HTML5) - i18n keys for all features (zh-CN + en)
This commit is contained in:
+6
-6
@@ -42,12 +42,12 @@
|
|||||||
|
|
||||||
## P3 — 体验优化
|
## P3 — 体验优化
|
||||||
|
|
||||||
- [ ] **右键菜单** — key 列表右键:复制/删除/多选/导出/内存分析/加载文件夹/删除文件夹
|
- [x] **右键菜单** — key 列表右键:复制/删除/多选/导出/内存分析/加载文件夹/删除文件夹
|
||||||
- [ ] **多选增强** — Shift+Click 范围选择,全选复选框
|
- [x] **多选增强** — Shift+Click 范围选择,全选复选框
|
||||||
- [ ] **快捷键增强** — Ctrl+,(设置) / Ctrl+G(命令日志) / Ctrl+W(关闭标签) / Ctrl+S(保存) / Ctrl+L(清CLI) / Ctrl+/(帮助)
|
- [x] **快捷键增强** — Ctrl+,(设置) / Ctrl+G(命令日志) / Ctrl+W(关闭标签) / Ctrl+S(保存) / Ctrl+L(清CLI) / Ctrl+/(帮助)
|
||||||
- [ ] **连接颜色标记** — 每个连接可选颜色(预定义色板)
|
- [x] **连接颜色标记** — 每个连接可选颜色(预定义色板)
|
||||||
- [ ] **连接拖拽排序** — Sortable.js 拖拽重排连接列表
|
- [x] **连接拖拽排序** — Sortable.js 拖拽重排连接列表
|
||||||
- [ ] **复制连接** — 右键复制现有连接配置
|
- [x] **复制连接** — 右键复制现有连接配置
|
||||||
- [ ] **侧边栏可拖拽** — 调整宽度 200–1500px,持久化
|
- [ ] **侧边栏可拖拽** — 调整宽度 200–1500px,持久化
|
||||||
- [ ] **DB 自定义名称** — 用户可重命名 DB0 → 生产库,下拉显示
|
- [ ] **DB 自定义名称** — 用户可重命名 DB0 → 生产库,下拉显示
|
||||||
- [ ] **DB key 数量显示** — 下拉框显示每个 DB 的 key 数量
|
- [ ] **DB key 数量显示** — 下拉框显示每个 DB 的 key 数量
|
||||||
|
|||||||
@@ -131,6 +131,14 @@ async function execute() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle SELECT — update current DB
|
||||||
|
if (command === 'SELECT' && args.length > 0) {
|
||||||
|
const dbNum = parseInt(args[0], 10)
|
||||||
|
if (!isNaN(dbNum)) {
|
||||||
|
connStore.currentDb = dbNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await window.electronAPI.redis.execute(connStore.activeId, command, args)
|
const result = await window.electronAPI.redis.execute(connStore.activeId, command, args)
|
||||||
let formatted = ''
|
let formatted = ''
|
||||||
|
|
||||||
@@ -155,6 +163,27 @@ async function execute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
history.value.push({ cmd, result: formatted })
|
history.value.push({ cmd, result: formatted })
|
||||||
|
|
||||||
|
// Dispatch refresh-key-list for write commands
|
||||||
|
const writeCommands = new Set([
|
||||||
|
'SET', 'SETEX', 'SETNX', 'PSETEX', 'MSET', 'MSETNX', 'APPEND', 'GETSET', 'GETDEL',
|
||||||
|
'HSET', 'HSETNX', 'HMSET', 'HDEL',
|
||||||
|
'LPUSH', 'LPUSHX', 'RPUSH', 'RPUSHX', 'LSET', 'LINSERT', 'LREM', 'LPOP', 'RPOP',
|
||||||
|
'SADD', 'SREM', 'SMOVE', 'SPOP', 'SINTERSTORE', 'SUNIONSTORE', 'SDIFFSTORE',
|
||||||
|
'ZADD', 'ZREM', 'ZINCRBY', 'ZPOPMIN', 'ZPOPMAX', 'ZINTERSTORE', 'ZUNIONSTORE',
|
||||||
|
'XADD', 'XDEL', 'XTRIM', 'XSETID', 'XGROUP', 'XGROUP', 'XGROUP',
|
||||||
|
'DEL', 'UNLINK', 'RENAME', 'RENAMENX',
|
||||||
|
'EXPIRE', 'EXPIREAT', 'PEXPIRE', 'PEXPIREAT', 'PERSIST',
|
||||||
|
'FLUSHDB', 'FLUSHALL', 'SWAPDB', 'MOVE', 'COPY',
|
||||||
|
'GEOADD', 'GEODEL',
|
||||||
|
'PFADD', 'PFMERGE',
|
||||||
|
'BITOP', 'SETBIT',
|
||||||
|
'JSON.SET', 'JSON.DEL', 'JSON.ARRAPPEND', 'JSON.ARRINSERT', 'JSON.ARRPOP',
|
||||||
|
'JSON.NUMINCRBY', 'JSON.STRAPPEND',
|
||||||
|
])
|
||||||
|
if (writeCommands.has(command)) {
|
||||||
|
window.dispatchEvent(new CustomEvent('refresh-key-list'))
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
history.value.push({ cmd, result: `ERROR: ${err.message}`, error: true })
|
history.value.push({ cmd, result: `ERROR: ${err.message}`, error: true })
|
||||||
}
|
}
|
||||||
@@ -345,15 +374,21 @@ function onMonitorMessage(msg: any) {
|
|||||||
nextTick(() => { if (outputRef.value) outputRef.value.scrollTop = outputRef.value.scrollHeight })
|
nextTick(() => { if (outputRef.value) outputRef.value.scrollTop = outputRef.value.scrollHeight })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClearCli() {
|
||||||
|
history.value = []
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.electronAPI.redis.onSubscribeMessage(onSubscribeMessage)
|
window.electronAPI.redis.onSubscribeMessage(onSubscribeMessage)
|
||||||
window.electronAPI.redis.onMonitorMessage(onMonitorMessage)
|
window.electronAPI.redis.onMonitorMessage(onMonitorMessage)
|
||||||
|
window.addEventListener('clear-cli', onClearCli)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (connStore.activeId && streamingMode.value !== 'none') {
|
if (connStore.activeId && streamingMode.value !== 'none') {
|
||||||
stopStreaming()
|
stopStreaming()
|
||||||
}
|
}
|
||||||
|
window.removeEventListener('clear-cli', onClearCli)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// src/renderer/src/components/ConnectionCard.vue
|
// src/renderer/src/components/ConnectionCard.vue
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import type { ConnectionConfig } from '@renderer/stores/connection'
|
import type { ConnectionConfig } from '@renderer/stores/connection'
|
||||||
import { useConnectionStore } from '@renderer/stores/connection'
|
import { useConnectionStore } from '@renderer/stores/connection'
|
||||||
|
import { useI18n } from '@renderer/i18n'
|
||||||
|
|
||||||
const props = defineProps<{ connection: ConnectionConfig }>()
|
const props = defineProps<{ connection: ConnectionConfig }>()
|
||||||
const emit = defineEmits<{ edit: [conn: ConnectionConfig]; delete: [id: string] }>()
|
const emit = defineEmits<{ edit: [conn: ConnectionConfig]; delete: [id: string] }>()
|
||||||
const connStore = useConnectionStore()
|
const connStore = useConnectionStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const isActive = computed(() => connStore.activeId === props.connection.id)
|
const isActive = computed(() => connStore.activeId === props.connection.id)
|
||||||
const isConn = computed(() => connStore.isConnectedById(props.connection.id))
|
const isConn = computed(() => connStore.isConnectedById(props.connection.id))
|
||||||
@@ -15,6 +18,11 @@ const statusColor = computed(() => {
|
|||||||
return 'var(--text-muted)'
|
return 'var(--text-muted)'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const COLOR_PRESETS = ['#6366f1', '#22c55e', '#f59e0b', '#ef4444', '#3b82f6', '#ec4899']
|
||||||
|
const showColorPicker = ref(false)
|
||||||
|
|
||||||
|
const assignedColor = computed(() => props.connection.color || '')
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (isConn.value) {
|
if (isConn.value) {
|
||||||
if (isActive.value) {
|
if (isActive.value) {
|
||||||
@@ -26,14 +34,63 @@ function handleClick() {
|
|||||||
connStore.connect(props.connection)
|
connStore.connect(props.connection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cycleColor() {
|
||||||
|
if (!props.connection.color) {
|
||||||
|
setColor(COLOR_PRESETS[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const idx = COLOR_PRESETS.indexOf(props.connection.color)
|
||||||
|
if (idx === -1 || idx === COLOR_PRESETS.length - 1) {
|
||||||
|
setColor('')
|
||||||
|
} else {
|
||||||
|
setColor(COLOR_PRESETS[idx + 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColor(color: string) {
|
||||||
|
const updated = { ...props.connection, color: color || undefined }
|
||||||
|
connStore.saveConnection(updated)
|
||||||
|
showColorPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectColor(color: string) {
|
||||||
|
setColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDuplicate() {
|
||||||
|
const copy: ConnectionConfig = {
|
||||||
|
...props.connection,
|
||||||
|
id: `${props.connection.id}_copy_${Date.now()}`,
|
||||||
|
name: `${props.connection.name} (copy)`,
|
||||||
|
order: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
}
|
||||||
|
await connStore.saveConnection(copy)
|
||||||
|
ElMessage.success(t('connection.duplicated'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag events
|
||||||
|
function onDragStart(e: DragEvent) {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.setData('text/plain', props.connection.id)
|
||||||
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="connection-card"
|
class="connection-card"
|
||||||
:class="{ active: isActive, connecting: connStore.connecting && connStore.activeId === connection.id }"
|
:class="{ active: isActive, connecting: connStore.connecting && connStore.activeId === connection.id }"
|
||||||
|
:style="{
|
||||||
|
'--status-color': statusColor,
|
||||||
|
'--card-color': assignedColor || 'transparent',
|
||||||
|
'border-left': assignedColor ? `3px solid ${assignedColor}` : undefined,
|
||||||
|
}"
|
||||||
|
draggable="true"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
:style="{ '--status-color': statusColor }"
|
@dragstart="onDragStart"
|
||||||
>
|
>
|
||||||
<div class="card-status" />
|
<div class="card-status" />
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -41,6 +98,32 @@ function handleClick() {
|
|||||||
<div class="card-host">{{ connection.host }}:{{ connection.port }}</div>
|
<div class="card-host">{{ connection.host }}:{{ connection.port }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions" @click.stop>
|
<div class="card-actions" @click.stop>
|
||||||
|
<!-- Color dot / picker -->
|
||||||
|
<div class="color-wrapper" @mouseenter="showColorPicker = true" @mouseleave="showColorPicker = false">
|
||||||
|
<button
|
||||||
|
class="card-btn color-dot"
|
||||||
|
:style="{ background: assignedColor || 'var(--text-muted)' }"
|
||||||
|
:title="t('connection.setColor')"
|
||||||
|
@click="cycleColor"
|
||||||
|
/>
|
||||||
|
<div v-if="showColorPicker" class="color-dropdown">
|
||||||
|
<button
|
||||||
|
v-for="c in COLOR_PRESETS"
|
||||||
|
:key="c"
|
||||||
|
class="color-option"
|
||||||
|
:style="{ background: c }"
|
||||||
|
:class="{ active: assignedColor === c }"
|
||||||
|
@click="selectColor(c)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="assignedColor"
|
||||||
|
class="color-option color-clear"
|
||||||
|
title="Clear"
|
||||||
|
@click="setColor('')"
|
||||||
|
>✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="card-btn" :title="t('connection.duplicate')" @click="handleDuplicate">📋</button>
|
||||||
<button class="card-btn" title="Edit" @click="emit('edit', connection)">✎</button>
|
<button class="card-btn" title="Edit" @click="emit('edit', connection)">✎</button>
|
||||||
<button class="card-btn card-btn-danger" title="Delete" @click="emit('delete', connection.id)">✕</button>
|
<button class="card-btn card-btn-danger" title="Delete" @click="emit('delete', connection.id)">✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,6 +194,7 @@ function handleClick() {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-card:hover .card-actions {
|
.connection-card:hover .card-actions {
|
||||||
@@ -140,4 +224,61 @@ function handleClick() {
|
|||||||
background: rgba(239, 68, 68, 0.15);
|
background: rgba(239, 68, 68, 0.15);
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Color picker */
|
||||||
|
.color-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-dot {
|
||||||
|
width: 14px !important;
|
||||||
|
height: 14px !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 6px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 6px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-option {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-option:hover {
|
||||||
|
transform: scale(1.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-option.active {
|
||||||
|
border-color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-clear {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
background: var(--bg-card-hover) !important;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { ConnectionConfig } from '@renderer/stores/connection'
|
|||||||
const connStore = useConnectionStore()
|
const connStore = useConnectionStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
const dragOverId = ref<string | null>(null)
|
||||||
const emit = defineEmits<{ edit: [conn: ConnectionConfig | null] }>()
|
const emit = defineEmits<{ edit: [conn: ConnectionConfig | null] }>()
|
||||||
|
|
||||||
const filteredConnections = computed(() => {
|
const filteredConnections = computed(() => {
|
||||||
@@ -38,6 +39,40 @@ function handleNew() {
|
|||||||
emit('edit', null)
|
emit('edit', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drag and drop reorder
|
||||||
|
function onDragOver(e: DragEvent, id: string) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.dropEffect = 'move'
|
||||||
|
}
|
||||||
|
dragOverId.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragLeave() {
|
||||||
|
dragOverId.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDrop(e: DragEvent, targetId: string) {
|
||||||
|
e.preventDefault()
|
||||||
|
dragOverId.value = null
|
||||||
|
const sourceId = e.dataTransfer?.getData('text/plain')
|
||||||
|
if (!sourceId || sourceId === targetId) return
|
||||||
|
|
||||||
|
const ids = connStore.connections
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.order - b.order)
|
||||||
|
.map(c => c.id)
|
||||||
|
|
||||||
|
const sourceIdx = ids.indexOf(sourceId)
|
||||||
|
const targetIdx = ids.indexOf(targetId)
|
||||||
|
if (sourceIdx === -1 || targetIdx === -1) return
|
||||||
|
|
||||||
|
ids.splice(sourceIdx, 1)
|
||||||
|
ids.splice(targetIdx, 0, sourceId)
|
||||||
|
|
||||||
|
await connStore.reorderConnections(ids)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
try {
|
try {
|
||||||
const data = await connStore.exportConnections()
|
const data = await connStore.exportConnections()
|
||||||
@@ -82,13 +117,21 @@ async function handleImport() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-items">
|
<div class="list-items">
|
||||||
<ConnectionCard
|
<div
|
||||||
v-for="conn in filteredConnections"
|
v-for="conn in filteredConnections"
|
||||||
:key="conn.id"
|
:key="conn.id"
|
||||||
|
class="drag-wrapper"
|
||||||
|
:class="{ 'drag-over': dragOverId === conn.id }"
|
||||||
|
@dragover="onDragOver($event, conn.id)"
|
||||||
|
@dragleave="onDragLeave"
|
||||||
|
@drop="onDrop($event, conn.id)"
|
||||||
|
>
|
||||||
|
<ConnectionCard
|
||||||
:connection="conn"
|
:connection="conn"
|
||||||
@edit="handleEdit"
|
@edit="handleEdit"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<div v-if="filteredConnections.length === 0" class="list-empty">
|
<div v-if="filteredConnections.length === 0" class="list-empty">
|
||||||
<p v-if="searchQuery">{{ t('connection.noMatch') }}</p>
|
<p v-if="searchQuery">{{ t('connection.noMatch') }}</p>
|
||||||
<p v-else>{{ t('connection.noConnections') }}</p>
|
<p v-else>{{ t('connection.noConnections') }}</p>
|
||||||
@@ -157,6 +200,15 @@ async function handleImport() {
|
|||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drag-wrapper {
|
||||||
|
transition: opacity 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-wrapper.drag-over {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
.list-footer {
|
.list-footer {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
|
|||||||
+286
-5
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// src/components/KeyList.vue
|
// src/components/KeyList.vue
|
||||||
import { ref, watch, computed } from 'vue'
|
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useVirtualizer } from '@tanstack/vue-virtual'
|
import { useVirtualizer } from '@tanstack/vue-virtual'
|
||||||
import { useKeyStore } from '@renderer/stores/key'
|
import { useKeyStore } from '@renderer/stores/key'
|
||||||
import { useConnectionStore } from '@renderer/stores/connection'
|
import { useConnectionStore } from '@renderer/stores/connection'
|
||||||
@@ -19,6 +19,14 @@ const multiSelect = ref(false)
|
|||||||
const selectedKeys = ref<Set<string>>(new Set())
|
const selectedKeys = ref<Set<string>>(new Set())
|
||||||
const expandedFolders = ref<Set<string>>(new Set())
|
const expandedFolders = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// Right-click context menu
|
||||||
|
const contextMenu = ref<{ visible: boolean; x: number; y: number; key: string }>({
|
||||||
|
visible: false, x: 0, y: 0, key: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Shift+Click range selection
|
||||||
|
const lastClickedIndex = ref<number>(-1)
|
||||||
|
|
||||||
// Virtual scrolling
|
// Virtual scrolling
|
||||||
const scrollRef = ref<HTMLElement>()
|
const scrollRef = ref<HTMLElement>()
|
||||||
const useVirtualization = computed(() => viewMode.value === 'list' && filteredKeys.value.length > 100)
|
const useVirtualization = computed(() => viewMode.value === 'list' && filteredKeys.value.length > 100)
|
||||||
@@ -103,8 +111,28 @@ async function refreshKeys() {
|
|||||||
await keyStore.scanKeys(connStore.activeId, '0', true)
|
await keyStore.scanKeys(connStore.activeId, '0', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectKey(keyName: string) {
|
async function selectKey(keyName: string, event?: MouseEvent) {
|
||||||
if (!connStore.activeId) return
|
if (!connStore.activeId) return
|
||||||
|
|
||||||
|
// Shift+Click range selection
|
||||||
|
if (event?.shiftKey && lastClickedIndex.value >= 0) {
|
||||||
|
const currentIndex = filteredKeys.value.indexOf(keyName)
|
||||||
|
if (currentIndex < 0) return
|
||||||
|
const start = Math.min(lastClickedIndex.value, currentIndex)
|
||||||
|
const end = Math.max(lastClickedIndex.value, currentIndex)
|
||||||
|
|
||||||
|
if (!multiSelect.value) {
|
||||||
|
multiSelect.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
selectedKeys.value.add(filteredKeys.value[i])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastClickedIndex.value = filteredKeys.value.indexOf(keyName)
|
||||||
|
|
||||||
if (multiSelect.value) {
|
if (multiSelect.value) {
|
||||||
if (selectedKeys.value.has(keyName)) {
|
if (selectedKeys.value.has(keyName)) {
|
||||||
selectedKeys.value.delete(keyName)
|
selectedKeys.value.delete(keyName)
|
||||||
@@ -260,6 +288,114 @@ function deselectAll() {
|
|||||||
keyStore.keyData = null
|
keyStore.keyData = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Right-click context menu handlers
|
||||||
|
function onKeyContextMenu(e: MouseEvent, keyName: string) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
contextMenu.value = { visible: true, x: e.clientX, y: e.clientY, key: keyName }
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeContextMenu() {
|
||||||
|
contextMenu.value.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyKeyName(keyName: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(keyName)
|
||||||
|
ElMessage.success(t('key.copied'))
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('Failed to copy')
|
||||||
|
}
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteKeyContext(keyName: string) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
t('key.deleteConfirm', { key: formatKeyName(keyName) }),
|
||||||
|
t('common.confirm'),
|
||||||
|
{ confirmButtonText: t('common.delete'), cancelButtonText: t('common.cancel'), type: 'warning' }
|
||||||
|
)
|
||||||
|
if (connStore.activeId) {
|
||||||
|
await window.electronAPI.redis.deleteKey(connStore.activeId, keyName)
|
||||||
|
ElMessage.success(t('key.deleted'))
|
||||||
|
await refreshKeys()
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err !== 'cancel') ElMessage.error(err.message)
|
||||||
|
}
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportSingleKey(keyName: string) {
|
||||||
|
selectedKeys.value.add(keyName)
|
||||||
|
await exportKeys()
|
||||||
|
selectedKeys.value.delete(keyName)
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function memoryUsage(keyName: string) {
|
||||||
|
if (!connStore.activeId) return
|
||||||
|
try {
|
||||||
|
const size = await window.electronAPI.redis.memoryUsage(connStore.activeId, keyName)
|
||||||
|
if (size !== null) {
|
||||||
|
ElMessage.success(t('key.memoryUsageResult', { size: size.toLocaleString() }))
|
||||||
|
} else {
|
||||||
|
ElMessage.info('Memory usage: N/A')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('Failed to get memory usage')
|
||||||
|
}
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyValue(keyName: string) {
|
||||||
|
if (!connStore.activeId) return
|
||||||
|
try {
|
||||||
|
const value = await window.electronAPI.redis.getString(connStore.activeId, keyName)
|
||||||
|
if (value !== null) {
|
||||||
|
await navigator.clipboard.writeText(value)
|
||||||
|
ElMessage.success(t('key.copied'))
|
||||||
|
} else {
|
||||||
|
ElMessage.info('No value or not a string type')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('Failed to copy value')
|
||||||
|
}
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAll() {
|
||||||
|
if (selectedKeys.value.size === filteredKeys.value.length) {
|
||||||
|
selectedKeys.value.clear()
|
||||||
|
} else {
|
||||||
|
for (const key of filteredKeys.value) {
|
||||||
|
selectedKeys.value.add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document-level listeners for context menu
|
||||||
|
function onDocumentClick() {
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDocumentKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', onDocumentClick)
|
||||||
|
document.addEventListener('keydown', onDocumentKeyDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', onDocumentClick)
|
||||||
|
document.removeEventListener('keydown', onDocumentKeyDown)
|
||||||
|
})
|
||||||
|
|
||||||
function toggleFolder(path: string) {
|
function toggleFolder(path: string) {
|
||||||
if (expandedFolders.value.has(path)) {
|
if (expandedFolders.value.has(path)) {
|
||||||
expandedFolders.value.delete(path)
|
expandedFolders.value.delete(path)
|
||||||
@@ -351,6 +487,23 @@ watch(filterText, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({ deselectAll })
|
defineExpose({ deselectAll })
|
||||||
|
|
||||||
|
// Listen for refresh-key-list event from CLI write commands
|
||||||
|
function onRefreshKeyList() {
|
||||||
|
if (connStore.activeId) {
|
||||||
|
keyStore.pattern = patternInput.value || '*'
|
||||||
|
keyStore.scanKeys(connStore.activeId, '0', true)
|
||||||
|
ElMessage.success(t('key.refreshedByCli'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('refresh-key-list', onRefreshKeyList)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('refresh-key-list', onRefreshKeyList)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -402,6 +555,17 @@ defineExpose({ deselectAll })
|
|||||||
<el-button size="small" @click="importKeys" :disabled="!connStore.activeId" class="import-btn">
|
<el-button size="small" @click="importKeys" :disabled="!connStore.activeId" class="import-btn">
|
||||||
{{ t('key.importKeys') }}
|
{{ t('key.importKeys') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<div v-if="multiSelect" class="select-all-wrapper">
|
||||||
|
<label class="select-all-label" @click.prevent="selectAll">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="select-all-checkbox"
|
||||||
|
:checked="multiSelect && selectedKeys.size === filteredKeys.length && filteredKeys.length > 0"
|
||||||
|
:indeterminate="multiSelect && selectedKeys.size > 0 && selectedKeys.size < filteredKeys.length"
|
||||||
|
/>
|
||||||
|
<span>{{ t('key.selectAll') }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<el-button-group class="key-actions">
|
<el-button-group class="key-actions">
|
||||||
<el-button size="small" @click="refreshKeys" :disabled="!connStore.activeId">
|
<el-button size="small" @click="refreshKeys" :disabled="!connStore.activeId">
|
||||||
<el-icon><Refresh /></el-icon>
|
<el-icon><Refresh /></el-icon>
|
||||||
@@ -466,7 +630,8 @@ defineExpose({ deselectAll })
|
|||||||
active: isKeySelected(filteredKeys[vItem.index]),
|
active: isKeySelected(filteredKeys[vItem.index]),
|
||||||
selected: isKeyMultiSelected(filteredKeys[vItem.index]),
|
selected: isKeyMultiSelected(filteredKeys[vItem.index]),
|
||||||
}"
|
}"
|
||||||
@click="selectKey(filteredKeys[vItem.index])"
|
@click="selectKey(filteredKeys[vItem.index], $event)"
|
||||||
|
@contextmenu.prevent="onKeyContextMenu($event, filteredKeys[vItem.index])"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="multiSelect"
|
v-if="multiSelect"
|
||||||
@@ -491,7 +656,8 @@ defineExpose({ deselectAll })
|
|||||||
active: isKeySelected(keyName),
|
active: isKeySelected(keyName),
|
||||||
selected: isKeyMultiSelected(keyName),
|
selected: isKeyMultiSelected(keyName),
|
||||||
}"
|
}"
|
||||||
@click="selectKey(keyName)"
|
@click="selectKey(keyName, $event)"
|
||||||
|
@contextmenu.prevent="onKeyContextMenu($event, keyName)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="multiSelect"
|
v-if="multiSelect"
|
||||||
@@ -523,7 +689,11 @@ defineExpose({ deselectAll })
|
|||||||
<span class="tree-count">{{ node.count }}</span>
|
<span class="tree-count">{{ node.count }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="tree-children" :class="{ hidden: !isFolderExpanded(node.fullPath) }">
|
<div v-else class="tree-children" :class="{ hidden: !isFolderExpanded(node.fullPath) }">
|
||||||
<div class="tree-item leaf-item" @click="selectKey(node.fullPath)">
|
<div
|
||||||
|
class="tree-item leaf-item"
|
||||||
|
@click="selectKey(node.fullPath, $event)"
|
||||||
|
@contextmenu.prevent="onKeyContextMenu($event, node.fullPath)"
|
||||||
|
>
|
||||||
<span class="tree-spacer"></span>
|
<span class="tree-spacer"></span>
|
||||||
<el-icon class="tree-icon"><Document /></el-icon>
|
<el-icon class="tree-icon"><Document /></el-icon>
|
||||||
<span class="tree-name">{{ formatKeyName(node.name) }}</span>
|
<span class="tree-name">{{ formatKeyName(node.name) }}</span>
|
||||||
@@ -533,6 +703,37 @@ defineExpose({ deselectAll })
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Right-click context menu -->
|
||||||
|
<div
|
||||||
|
v-if="contextMenu.visible"
|
||||||
|
class="key-context-menu"
|
||||||
|
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="key-context-item" @click="copyKeyName(contextMenu.key)">
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
<span>{{ t('key.copyKeyName') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="key-context-item" @click="copyValue(contextMenu.key)">
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
<span>{{ t('key.copyValue') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="key-context-divider" />
|
||||||
|
<div class="key-context-item" @click="memoryUsage(contextMenu.key)">
|
||||||
|
<el-icon><Odometer /></el-icon>
|
||||||
|
<span>{{ t('key.memoryUsage') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="key-context-item" @click="exportSingleKey(contextMenu.key)">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
<span>{{ t('key.exportKeys') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="key-context-divider" />
|
||||||
|
<div class="key-context-item key-context-item-danger" @click="deleteKeyContext(contextMenu.key)">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
<span>{{ t('common.delete') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="key-footer">
|
<div class="key-footer">
|
||||||
<span class="key-count">{{ t('key.count', { n: keyStore.keys.length }) }}</span>
|
<span class="key-count">{{ t('key.count', { n: keyStore.keys.length }) }}</span>
|
||||||
<div class="key-footer-actions">
|
<div class="key-footer-actions">
|
||||||
@@ -780,6 +981,86 @@ defineExpose({ deselectAll })
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Right-click context menu */
|
||||||
|
.key-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
min-width: 160px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-context-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-context-item:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-context-item-danger:hover {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-context-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-color);
|
||||||
|
margin: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-context-item .el-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-context-item:hover .el-icon {
|
||||||
|
color: var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-context-item-danger:hover .el-icon {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select All checkbox */
|
||||||
|
.select-all-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-all-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-all-label:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-all-checkbox {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
accent-color: var(--accent-light);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Search History Dropdown */
|
/* Search History Dropdown */
|
||||||
.pattern-input-wrapper {
|
.pattern-input-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// src/components/MainArea.vue
|
// src/components/MainArea.vue
|
||||||
import { ref, watch, computed, nextTick } from 'vue'
|
import { ref, watch, computed, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
import { useConnectionStore } from '@renderer/stores/connection'
|
import { useConnectionStore } from '@renderer/stores/connection'
|
||||||
import { useI18n } from '@renderer/i18n'
|
import { useI18n } from '@renderer/i18n'
|
||||||
import StatusView from './StatusView.vue'
|
import StatusView from './StatusView.vue'
|
||||||
@@ -188,6 +188,19 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Listen for toggle-settings event from keyboard shortcut
|
||||||
|
function onToggleSettings() {
|
||||||
|
emit('update:showSettings', !props.showSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('toggle-settings', onToggleSettings)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('toggle-settings', onToggleSettings)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -52,10 +52,50 @@ function refreshAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSettings() {
|
||||||
|
window.dispatchEvent(new CustomEvent('toggle-settings'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCli() {
|
||||||
|
window.dispatchEvent(new CustomEvent('clear-cli'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveKey() {
|
||||||
|
window.dispatchEvent(new CustomEvent('save-key'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHotkeys() {
|
||||||
|
const shortcuts = [
|
||||||
|
{ keys: 'Ctrl+N', desc: t('connection.new') },
|
||||||
|
{ keys: 'Ctrl+R / F5', desc: t('common.refresh') },
|
||||||
|
{ keys: 'Ctrl+G', desc: t('commandLog.title') },
|
||||||
|
{ keys: 'Ctrl+,', desc: t('settings.title') },
|
||||||
|
{ keys: 'Ctrl+S', desc: t('editor.save') },
|
||||||
|
{ keys: 'Ctrl+L', desc: t('cli.title') + ' ' + t('commandLog.clear') },
|
||||||
|
{ keys: 'Ctrl+/', desc: t('cli.hotkeys') },
|
||||||
|
{ keys: 'Delete', desc: t('common.delete') },
|
||||||
|
{ keys: 'Escape', desc: t('key.selectKey') },
|
||||||
|
]
|
||||||
|
const html = `<div style="font-size:13px;line-height:2">
|
||||||
|
${shortcuts.map(s => `<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--border-subtle)">
|
||||||
|
<span style="color:var(--text-secondary)">${s.desc}</span>
|
||||||
|
<kbd style="background:var(--bg-card);padding:2px 8px;border-radius:4px;font-family:monospace;font-size:12px;border:1px solid var(--border-color)">${s.keys}</kbd>
|
||||||
|
</div>`).join('')}
|
||||||
|
</div>`
|
||||||
|
ElMessageBox.alert(html, t('cli.hotkeysTitle'), {
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
confirmButtonText: t('common.ok'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useKeyboard({
|
useKeyboard({
|
||||||
'ctrl+n': handleNew,
|
'ctrl+n': handleNew,
|
||||||
'ctrl+r': refreshAll,
|
'ctrl+r': refreshAll,
|
||||||
'ctrl+g': () => { commandLogVisible.value = true },
|
'ctrl+g': () => { commandLogVisible.value = true },
|
||||||
|
'ctrl+,': toggleSettings,
|
||||||
|
'ctrl+s': saveKey,
|
||||||
|
'ctrl+l': clearCli,
|
||||||
|
'ctrl+/': showHotkeys,
|
||||||
'f5': refreshAll,
|
'f5': refreshAll,
|
||||||
'delete': deleteSelectedKey,
|
'delete': deleteSelectedKey,
|
||||||
'escape': deselectKey,
|
'escape': deselectKey,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// src/components/StringEditor.vue
|
// src/components/StringEditor.vue
|
||||||
import { ref, watch, computed } from 'vue'
|
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||||
import { useKeyStore } from '@renderer/stores/key'
|
import { useKeyStore } from '@renderer/stores/key'
|
||||||
import { useConnectionStore } from '@renderer/stores/connection'
|
import { useConnectionStore } from '@renderer/stores/connection'
|
||||||
@@ -129,6 +129,19 @@ async function save() {
|
|||||||
ElMessage.error(err.message)
|
ElMessage.error(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for save-key keyboard shortcut
|
||||||
|
function onSaveKey() {
|
||||||
|
if (isEditing.value) save()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('save-key', onSaveKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('save-key', onSaveKey)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ export default {
|
|||||||
testConnection: 'Test Connection',
|
testConnection: 'Test Connection',
|
||||||
create: 'Create',
|
create: 'Create',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
|
duplicate: 'Duplicate',
|
||||||
|
setColor: 'Set Color',
|
||||||
|
duplicated: 'Connection duplicated',
|
||||||
},
|
},
|
||||||
key: {
|
key: {
|
||||||
title: 'Keys',
|
title: 'Keys',
|
||||||
@@ -112,6 +115,11 @@ export default {
|
|||||||
importFailed: 'Import failed: {error}',
|
importFailed: 'Import failed: {error}',
|
||||||
binaryKey: 'Binary Key',
|
binaryKey: 'Binary Key',
|
||||||
copyHex: 'Copy Hex',
|
copyHex: 'Copy Hex',
|
||||||
|
copyKeyName: 'Copy Key Name',
|
||||||
|
copyValue: 'Copy Value',
|
||||||
|
memoryUsage: 'Memory Usage',
|
||||||
|
memoryUsageResult: 'Memory usage: {size} bytes',
|
||||||
|
selectAll: 'Select All',
|
||||||
},
|
},
|
||||||
types: {
|
types: {
|
||||||
string: 'STRING',
|
string: 'STRING',
|
||||||
@@ -189,6 +197,8 @@ export default {
|
|||||||
txPrompt: 'tx>',
|
txPrompt: 'tx>',
|
||||||
importCommands: 'Import Commands',
|
importCommands: 'Import Commands',
|
||||||
importCmdDone: 'Executed {n} commands',
|
importCmdDone: 'Executed {n} commands',
|
||||||
|
hotkeys: 'Hotkeys',
|
||||||
|
hotkeysTitle: 'Keyboard Shortcuts',
|
||||||
},
|
},
|
||||||
slowlog: {
|
slowlog: {
|
||||||
title: 'Slow Log',
|
title: 'Slow Log',
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ export default {
|
|||||||
testConnection: '测试连接',
|
testConnection: '测试连接',
|
||||||
create: '创建',
|
create: '创建',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
|
duplicate: '复制连接',
|
||||||
|
setColor: '设置颜色',
|
||||||
|
duplicated: '连接已复制',
|
||||||
},
|
},
|
||||||
key: {
|
key: {
|
||||||
title: '键',
|
title: '键',
|
||||||
@@ -112,6 +115,11 @@ export default {
|
|||||||
importFailed: '导入失败: {error}',
|
importFailed: '导入失败: {error}',
|
||||||
binaryKey: '二进制键',
|
binaryKey: '二进制键',
|
||||||
copyHex: '复制 Hex',
|
copyHex: '复制 Hex',
|
||||||
|
copyKeyName: '复制键名',
|
||||||
|
copyValue: '复制值',
|
||||||
|
memoryUsage: '内存占用',
|
||||||
|
memoryUsageResult: '内存占用: {size} 字节',
|
||||||
|
selectAll: '全选',
|
||||||
},
|
},
|
||||||
types: {
|
types: {
|
||||||
string: '字符串',
|
string: '字符串',
|
||||||
@@ -189,6 +197,8 @@ export default {
|
|||||||
txPrompt: 'tx>',
|
txPrompt: 'tx>',
|
||||||
importCommands: '导入命令',
|
importCommands: '导入命令',
|
||||||
importCmdDone: '已执行 {n} 条命令',
|
importCmdDone: '已执行 {n} 条命令',
|
||||||
|
hotkeys: '快捷键',
|
||||||
|
hotkeysTitle: '键盘快捷键',
|
||||||
},
|
},
|
||||||
slowlog: {
|
slowlog: {
|
||||||
title: '慢日志',
|
title: '慢日志',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const useConnectionStore = defineStore('connection', () => {
|
|||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const serverInfoMap = ref<Record<string, string>>({})
|
const serverInfoMap = ref<Record<string, string>>({})
|
||||||
const healthMap = ref<Record<string, boolean>>({})
|
const healthMap = ref<Record<string, boolean>>({})
|
||||||
|
const currentDb = ref<number>(0)
|
||||||
const healthTimers = new Map<string, ReturnType<typeof setInterval>>()
|
const healthTimers = new Map<string, ReturnType<typeof setInterval>>()
|
||||||
|
|
||||||
const activeConnection = computed(() =>
|
const activeConnection = computed(() =>
|
||||||
@@ -273,7 +274,7 @@ export const useConnectionStore = defineStore('connection', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connections, activeId, connectedIds, connecting, error, serverInfo, healthOk,
|
connections, activeId, connectedIds, connecting, error, serverInfo, healthOk, currentDb,
|
||||||
activeConnection, isConnected,
|
activeConnection, isConnected,
|
||||||
loadConnections, decryptPassword, getInfo, saveConnection, deleteConnection, reorderConnections,
|
loadConnections, decryptPassword, getInfo, saveConnection, deleteConnection, reorderConnections,
|
||||||
connect, disconnect, disconnectById, switchTo, isConnectedById, testConnection, exportConnections, importConnections,
|
connect, disconnect, disconnectById, switchTo, isConnectedById, testConnection, exportConnections, importConnections,
|
||||||
|
|||||||
Reference in New Issue
Block a user