feat: update monitor, terminal, and SFTP interaction flow
This commit is contained in:
@@ -1,38 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useConnectionsStore } from '../stores/connections'
|
||||
import { useSftpTabsStore } from '../stores/sftpTabs'
|
||||
import { useTerminalTabsStore } from '../stores/terminalTabs'
|
||||
import type { Connection, ConnectionCreateRequest } from '../api/connections'
|
||||
import ConnectionForm from '../components/ConnectionForm.vue'
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useConnectionsStore } from '../stores/connections'
|
||||
import { useSftpTabsStore } from '../stores/sftpTabs'
|
||||
import { useTerminalTabsStore } from '../stores/terminalTabs'
|
||||
import type { Connection, ConnectionCreateRequest } from '../api/connections'
|
||||
import ConnectionForm from '../components/ConnectionForm.vue'
|
||||
import {
|
||||
Server,
|
||||
Plus,
|
||||
Terminal,
|
||||
FolderOpen,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Key,
|
||||
Lock,
|
||||
Search,
|
||||
X,
|
||||
} from 'lucide-vue-next'
|
||||
Pencil,
|
||||
Trash2,
|
||||
Key,
|
||||
Lock,
|
||||
Search,
|
||||
X,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useConnectionsStore()
|
||||
const tabsStore = useTerminalTabsStore()
|
||||
const sftpTabsStore = useSftpTabsStore()
|
||||
const route = useRoute()
|
||||
const store = useConnectionsStore()
|
||||
const tabsStore = useTerminalTabsStore()
|
||||
const sftpTabsStore = useSftpTabsStore()
|
||||
|
||||
const showForm = ref(false)
|
||||
const editingConn = ref<Connection | null>(null)
|
||||
const searchQuery = ref('')
|
||||
let searchDebounceTimer = 0
|
||||
|
||||
const keyword = computed(() => route.query.q as string || '')
|
||||
const filteredConnections = computed(() => {
|
||||
const q = keyword.value.trim().toLowerCase()
|
||||
|
||||
const q = searchQuery.value.trim().toLowerCase()
|
||||
|
||||
if (!q) {
|
||||
return store.connections
|
||||
}
|
||||
@@ -59,8 +60,11 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
watch(searchQuery, (val) => {
|
||||
updateSearchParam(val)
|
||||
})
|
||||
clearTimeout(searchDebounceTimer)
|
||||
searchDebounceTimer = window.setTimeout(() => {
|
||||
updateSearchParam(val)
|
||||
}, 300)
|
||||
})
|
||||
|
||||
function openCreate() {
|
||||
editingConn.value = null
|
||||
@@ -91,97 +95,97 @@ async function handleDelete(conn: Connection) {
|
||||
await store.deleteConnection(conn.id)
|
||||
}
|
||||
|
||||
function openTerminal(conn: Connection) {
|
||||
tabsStore.openTab(conn)
|
||||
router.push('/terminal')
|
||||
}
|
||||
function openTerminal(conn: Connection) {
|
||||
tabsStore.openTab(conn)
|
||||
router.push('/terminal')
|
||||
}
|
||||
|
||||
function openSftp(conn: Connection) {
|
||||
sftpTabsStore.openOrFocus(conn)
|
||||
router.push(`/sftp/${conn.id}`)
|
||||
}
|
||||
|
||||
function openSftp(conn: Connection) {
|
||||
sftpTabsStore.openOrFocus(conn)
|
||||
router.push(`/sftp/${conn.id}`)
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchQuery.value = ''
|
||||
updateSearchParam('')
|
||||
}
|
||||
|
||||
function highlightMatch(text: string): string {
|
||||
const q = keyword.value.trim()
|
||||
const q = searchQuery.value.trim()
|
||||
if (!q) return text
|
||||
|
||||
|
||||
const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const regex = new RegExp(escaped, 'gi')
|
||||
|
||||
|
||||
return text.replace(regex, (match) => `<span class="text-cyan-300 font-semibold">${match}</span>`)
|
||||
}
|
||||
</script>
|
||||
}
|
||||
</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"
|
||||
<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 class="relative mb-6">
|
||||
<Search class="w-4 h-4 text-slate-500 absolute left-3 top-1/2 -translate-y-1/2" aria-hidden="true" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索名称、主机、用户名或端口"
|
||||
class="w-full rounded-xl border border-slate-700 bg-slate-800/70 py-3 pl-10 pr-11 text-slate-100 placeholder:text-slate-500 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
|
||||
aria-label="搜索连接"
|
||||
/>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
@click="clearSearch"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 rounded-md p-1 text-slate-400 hover:bg-slate-700 hover:text-slate-100 transition-colors duration-200 cursor-pointer"
|
||||
aria-label="清空搜索"
|
||||
>
|
||||
<X class="w-4 h-4" 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" />
|
||||
添加连接
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative mb-6">
|
||||
<Search class="w-4 h-4 text-slate-500 absolute left-3 top-1/2 -translate-y-1/2" aria-hidden="true" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索名称、主机、用户名或端口"
|
||||
class="w-full rounded-xl border border-slate-700 bg-slate-800/70 py-3 pl-10 pr-11 text-slate-100 placeholder:text-slate-500 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
|
||||
aria-label="搜索连接"
|
||||
/>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
@click="clearSearch"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 rounded-md p-1 text-slate-400 hover:bg-slate-700 hover:text-slate-100 transition-colors duration-200 cursor-pointer"
|
||||
aria-label="清空搜索"
|
||||
>
|
||||
<X class="w-4 h-4" 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-if="filteredConnections.length === 0"
|
||||
class="text-center py-16 bg-slate-800/50 rounded-xl border border-slate-700"
|
||||
>
|
||||
<Search class="w-16 h-16 text-slate-500 mx-auto mb-4" aria-hidden="true" />
|
||||
<p class="text-slate-300 mb-2">未找到匹配的连接</p>
|
||||
<p class="text-sm text-slate-500 mb-4">试试搜索名称、主机、用户名或端口</p>
|
||||
<button
|
||||
@click="clearSearch"
|
||||
class="px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 text-slate-200 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 filteredConnections"
|
||||
:key="conn.id"
|
||||
class="bg-slate-800 rounded-xl border border-slate-700 p-4 hover:border-slate-600 transition-colors duration-200"
|
||||
>
|
||||
>
|
||||
添加第一个连接
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="filteredConnections.length === 0"
|
||||
class="text-center py-16 bg-slate-800/50 rounded-xl border border-slate-700"
|
||||
>
|
||||
<Search class="w-16 h-16 text-slate-500 mx-auto mb-4" aria-hidden="true" />
|
||||
<p class="text-slate-300 mb-2">未找到匹配的连接</p>
|
||||
<p class="text-sm text-slate-500 mb-4">试试搜索名称、主机、用户名或端口</p>
|
||||
<button
|
||||
@click="clearSearch"
|
||||
class="px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 text-slate-200 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 filteredConnections"
|
||||
: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">
|
||||
@@ -217,24 +221,24 @@ function highlightMatch(text: string): string {
|
||||
/>
|
||||
<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>
|
||||
<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"
|
||||
|
||||
Reference in New Issue
Block a user