Initial commit: SSH Manager (backend + frontend)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
liu
2026-02-03 09:10:06 +08:00
commit 1c5a44ff71
63 changed files with 6946 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useConnectionsStore } from '../stores/connections'
import type { Connection, ConnectionCreateRequest } from '../api/connections'
import ConnectionForm from '../components/ConnectionForm.vue'
import {
Server,
Plus,
Terminal,
FolderOpen,
Pencil,
Trash2,
Key,
Lock,
} from 'lucide-vue-next'
const router = useRouter()
const store = useConnectionsStore()
const showForm = ref(false)
const editingConn = ref<Connection | null>(null)
onMounted(() => store.fetchConnections())
function openCreate() {
editingConn.value = null
showForm.value = true
}
function openEdit(conn: Connection) {
editingConn.value = conn
showForm.value = true
}
function closeForm() {
showForm.value = false
editingConn.value = null
}
async function handleSave(data: ConnectionCreateRequest) {
if (editingConn.value) {
await store.updateConnection(editingConn.value.id, data)
} else {
await store.createConnection(data)
}
closeForm()
}
async function handleDelete(conn: Connection) {
if (!confirm(`确定删除连接「${conn.name}」?`)) return
await store.deleteConnection(conn.id)
}
function openTerminal(conn: Connection) {
router.push(`/terminal/${conn.id}`)
}
function openSftp(conn: Connection) {
router.push(`/sftp/${conn.id}`)
}
</script>
<template>
<div class="p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-slate-100">连接列表</h2>
<button
@click="openCreate"
class="flex items-center gap-2 px-4 py-2 rounded-lg bg-cyan-600 hover:bg-cyan-500 text-white font-medium transition-colors duration-200 cursor-pointer"
aria-label="添加连接"
>
<Plus class="w-5 h-5" aria-hidden="true" />
添加连接
</button>
</div>
<div v-if="store.connections.length === 0" class="text-center py-16 bg-slate-800/50 rounded-xl border border-slate-700">
<Server class="w-16 h-16 text-slate-500 mx-auto mb-4" aria-hidden="true" />
<p class="text-slate-400 mb-4">暂无连接</p>
<button
@click="openCreate"
class="px-4 py-2 rounded-lg bg-cyan-600 hover:bg-cyan-500 text-white transition-colors duration-200 cursor-pointer"
>
添加第一个连接
</button>
</div>
<div v-else class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="conn in store.connections"
:key="conn.id"
class="bg-slate-800 rounded-xl border border-slate-700 p-4 hover:border-slate-600 transition-colors duration-200"
>
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-2">
<div class="w-10 h-10 rounded-lg bg-slate-700 flex items-center justify-center">
<Server class="w-5 h-5 text-cyan-400" aria-hidden="true" />
</div>
<div>
<h3 class="font-medium text-slate-100">{{ conn.name }}</h3>
<p class="text-sm text-slate-400">{{ conn.username }}@{{ conn.host }}:{{ conn.port }}</p>
</div>
</div>
<div class="flex items-center gap-1">
<button
@click="openEdit(conn)"
class="p-2 rounded-lg text-slate-400 hover:bg-slate-700 hover:text-slate-100 transition-colors duration-200 cursor-pointer"
aria-label="编辑"
>
<Pencil class="w-4 h-4" aria-hidden="true" />
</button>
<button
@click="handleDelete(conn)"
class="p-2 rounded-lg text-slate-400 hover:bg-red-900/50 hover:text-red-400 transition-colors duration-200 cursor-pointer"
aria-label="删除"
>
<Trash2 class="w-4 h-4" aria-hidden="true" />
</button>
</div>
</div>
<div class="flex items-center gap-2 mb-3">
<component
:is="conn.authType === 'PRIVATE_KEY' ? Key : Lock"
class="w-4 h-4 text-slate-500"
aria-hidden="true"
/>
<span class="text-sm text-slate-500">{{ conn.authType === 'PRIVATE_KEY' ? '私钥' : '密码' }}</span>
</div>
<div class="flex gap-2">
<button
@click="openTerminal(conn)"
class="flex-1 flex items-center justify-center gap-2 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 text-slate-300 hover:text-slate-100 transition-colors duration-200 cursor-pointer text-sm"
>
<Terminal class="w-4 h-4" aria-hidden="true" />
终端
</button>
<button
@click="openSftp(conn)"
class="flex-1 flex items-center justify-center gap-2 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 text-slate-300 hover:text-slate-100 transition-colors duration-200 cursor-pointer text-sm"
>
<FolderOpen class="w-4 h-4" aria-hidden="true" />
文件
</button>
</div>
</div>
</div>
<ConnectionForm
v-if="showForm"
:connection="editingConn"
:on-save="handleSave"
@close="closeForm"
/>
</div>
</template>