5505434325
- 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)
285 lines
6.7 KiB
Vue
285 lines
6.7 KiB
Vue
<script setup lang="ts">
|
|
// src/renderer/src/components/ConnectionCard.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))
|
|
const statusColor = computed(() => {
|
|
if (isConn.value) return 'var(--success)'
|
|
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) {
|
|
connStore.disconnect()
|
|
} else {
|
|
connStore.switchTo(props.connection.id)
|
|
}
|
|
} else {
|
|
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"
|
|
@dragstart="onDragStart"
|
|
>
|
|
<div class="card-status" />
|
|
<div class="card-body">
|
|
<div class="card-name">{{ connection.name }}</div>
|
|
<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('')"
|
|
>✕</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 card-btn-danger" title="Delete" @click="emit('delete', connection.id)">✕</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.connection-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--radius-md);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.connection-card:hover {
|
|
background: var(--bg-card-hover);
|
|
transform: perspective(600px) rotateY(-1deg);
|
|
}
|
|
|
|
.connection-card.active {
|
|
background: var(--bg-card-active);
|
|
border-color: var(--border-accent);
|
|
box-shadow: var(--shadow-glow);
|
|
}
|
|
|
|
.connection-card.connecting {
|
|
opacity: 0.7;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.card-status {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--status-color);
|
|
box-shadow: 0 0 8px var(--status-color);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.card-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.card-name {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.card-host {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.card-actions {
|
|
display: flex;
|
|
gap: 4px;
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
align-items: center;
|
|
}
|
|
|
|
.connection-card:hover .card-actions {
|
|
opacity: 1;
|
|
}
|
|
|
|
.card-btn {
|
|
width: 24px;
|
|
height: 24px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.card-btn:hover {
|
|
background: var(--bg-card-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.card-btn-danger:hover {
|
|
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>
|