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:
2026-07-07 22:34:48 +08:00
parent b802fc6928
commit 5505434325
11 changed files with 617 additions and 21 deletions
+143 -2
View File
@@ -1,12 +1,15 @@
<script setup lang="ts">
// 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 { useConnectionStore } from '@renderer/stores/connection'
import { useI18n } from '@renderer/i18n'
const props = defineProps<{ connection: ConnectionConfig }>()
const emit = defineEmits<{ edit: [conn: ConnectionConfig]; delete: [id: string] }>()
const connStore = useConnectionStore()
const { t } = useI18n()
const isActive = computed(() => connStore.activeId === props.connection.id)
const isConn = computed(() => connStore.isConnectedById(props.connection.id))
@@ -15,6 +18,11 @@ const statusColor = computed(() => {
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() {
if (isConn.value) {
if (isActive.value) {
@@ -26,14 +34,63 @@ function handleClick() {
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>
<template>
<div
class="connection-card"
: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"
:style="{ '--status-color': statusColor }"
@dragstart="onDragStart"
>
<div class="card-status" />
<div class="card-body">
@@ -41,6 +98,32 @@ function handleClick() {
<div class="card-host">{{ connection.host }}:{{ connection.port }}</div>
</div>
<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('')"
>&#x2715;</button>
</div>
</div>
<button class="card-btn" :title="t('connection.duplicate')" @click="handleDuplicate">&#x1F4CB;</button>
<button class="card-btn" title="Edit" @click="emit('edit', connection)">&#x270E;</button>
<button class="card-btn card-btn-danger" title="Delete" @click="emit('delete', connection.id)">&#x2715;</button>
</div>
@@ -111,6 +194,7 @@ function handleClick() {
gap: 4px;
opacity: 0;
transition: opacity 0.15s;
align-items: center;
}
.connection-card:hover .card-actions {
@@ -140,4 +224,61 @@ function handleClick() {
background: rgba(239, 68, 68, 0.15);
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>