feat: Add quick connect group servers

This commit is contained in:
dalaolala committed 2026-03-02 17:25:57 +08:00
1 parent 838b344219
commit 2d07f270ae
3 files changed
+61 -17

No files matched your search

+4 -2
View File
@@ -154,8 +154,10 @@ webssh/
- **断开**: 点击"断开"按钮结束连接 - **断开**: 点击"断开"按钮结束连接
### 快速连接 ### 快速连接
- 不保存服务器信息,直接输入连接参数 - **无需提前配置**:直接输入凭据即可发起 SSH / SFTP 连接
- 适合临时连接或测试使用 - **独立多标签页架构**:每个终端和独立全屏 SFTP 文件管理器都运行在完全隔离的环境中,互不干扰
- **智能历史与分组**:连接信息(包括可选保存的密码)与分组展开状态自动安全保存在当前浏览器本地中
- **支持导入与导出**:一键导出 JSON 格式的快速连接数据,轻松在不同设备间迁移历史
## 🌐 Nginx 反向代理配置 ## 🌐 Nginx 反向代理配置
+1 -1
View File
@@ -140,7 +140,7 @@
<div class="feature-card" @click="$router.push('/quick-connect')" style="cursor: pointer;"> <div class="feature-card" @click="$router.push('/quick-connect')" style="cursor: pointer;">
<el-icon size="48" color="#F56C6C"><Connection /></el-icon> <el-icon size="48" color="#F56C6C"><Connection /></el-icon>
<h3>快速连接</h3> <h3>快速连接</h3>
<p>无需保存服务器信息,快速建立SSH连接</p> <p>服务器信息保存在本地,支持导入导出</p>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
+56 -14
View File
@@ -48,9 +48,11 @@
:data="treeData" :data="treeData"
:props="treeProps" :props="treeProps"
node-key="id" node-key="id"
default-expand-all :default-expanded-keys="expandedHistoryKeys"
highlight-current highlight-current
@node-click="handleTreeNodeClick" @node-click="handleTreeNodeClick"
@node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<div class="tree-node" :class="{ 'is-leaf': data.isLeaf }"> <div class="tree-node" :class="{ 'is-leaf': data.isLeaf }">
@@ -108,6 +110,24 @@
<el-input v-model="form.name" placeholder="请输入服务器名称(可选)" /> <el-input v-model="form.name" placeholder="请输入服务器名称(可选)" />
</el-form-item> </el-form-item>
<el-form-item label="所属分组" prop="group">
<el-select
v-model="form.group"
filterable
allow-create
default-first-option
placeholder="请输入或选择分组名称(可选)"
style="width: 100%"
>
<el-option
v-for="item in availableGroups"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="主机地址" prop="host"> <el-form-item label="主机地址" prop="host">
<el-input v-model="form.host" placeholder="请输入IP地址或域名" /> <el-input v-model="form.host" placeholder="请输入IP地址或域名" />
</el-form-item> </el-form-item>
@@ -211,7 +231,6 @@ import { useTerminalStore } from '@/stores/terminal'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
const HISTORY_KEY = 'webssh_quick_connect_history' const HISTORY_KEY = 'webssh_quick_connect_history'
const MAX_HISTORY = 20
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -227,6 +246,7 @@ const historyList = ref([])
const form = reactive({ const form = reactive({
name: '', name: '',
group: '',
host: '', host: '',
port: 22, port: 22,
username: '', username: '',
@@ -272,19 +292,30 @@ const rules = {
] ]
} }
// ========== 树形数据 ========== // ========== 树形数据 & 分组下拉 ==========
// 提取历史记录中出现过的所有分组(去重)
const availableGroups = computed(() => {
const groups = new Set()
historyList.value.forEach(item => {
if (item.group) {
groups.add(item.group)
}
})
return Array.from(groups).sort()
})
const treeProps = { const treeProps = {
children: 'children', children: 'children',
label: 'label' label: 'label'
} }
// 将 historyList 转换为树形结构,按主机地址分组 // 将 historyList 转换为树形结构,按分组名称分组
const treeData = computed(() => { const treeData = computed(() => {
const groups = {} const groups = {}
historyList.value.forEach((item, index) => { historyList.value.forEach((item, index) => {
const groupKey = item.host const groupKey = item.group || '未分组'
if (!groups[groupKey]) { if (!groups[groupKey]) {
groups[groupKey] = { groups[groupKey] = {
id: `group-${groupKey}`, id: `group-${groupKey}`,
@@ -313,6 +344,24 @@ const handleTreeNodeClick = (data) => {
} }
} }
// ========== 分组展开状态持久化 (sessionStorage) ==========
const EXPANDED_HISTORY_KEYS_KEY = 'webssh_quick_connect_expanded_keys'
const expandedHistoryKeys = ref(JSON.parse(sessionStorage.getItem(EXPANDED_HISTORY_KEYS_KEY) || '[]'))
const handleNodeExpand = (data) => {
if (!data.isLeaf && !expandedHistoryKeys.value.includes(data.id)) {
expandedHistoryKeys.value.push(data.id)
sessionStorage.setItem(EXPANDED_HISTORY_KEYS_KEY, JSON.stringify(expandedHistoryKeys.value))
}
}
const handleNodeCollapse = (data) => {
if (!data.isLeaf) {
expandedHistoryKeys.value = expandedHistoryKeys.value.filter(id => id !== data.id)
sessionStorage.setItem(EXPANDED_HISTORY_KEYS_KEY, JSON.stringify(expandedHistoryKeys.value))
}
}
// ========== 历史记录管理 ========== // ========== 历史记录管理 ==========
const loadHistory = () => { const loadHistory = () => {
@@ -331,6 +380,7 @@ const persistHistory = () => {
const saveToHistory = () => { const saveToHistory = () => {
const record = { const record = {
name: form.name || '', name: form.name || '',
group: form.group || '',
host: form.host, host: form.host,
port: form.port, port: form.port,
username: form.username, username: form.username,
@@ -356,15 +406,12 @@ const saveToHistory = () => {
historyList.value.unshift(record) historyList.value.unshift(record)
if (historyList.value.length > MAX_HISTORY) {
historyList.value = historyList.value.slice(0, MAX_HISTORY)
}
persistHistory() persistHistory()
} }
const fillFromHistory = (item) => { const fillFromHistory = (item) => {
form.name = item.name || '' form.name = item.name || ''
form.group = item.group || ''
form.host = item.host form.host = item.host
form.port = item.port form.port = item.port
form.username = item.username form.username = item.username
@@ -464,11 +511,6 @@ const handleImportFile = (event) => {
merged.unshift(item) merged.unshift(item)
}) })
// 限制最大条数
if (merged.length > MAX_HISTORY) {
merged = merged.slice(0, MAX_HISTORY)
}
historyList.value = merged historyList.value = merged
persistHistory() persistHistory()
ElMessage.success(`成功导入 ${addedCount} 条连接记录`) ElMessage.success(`成功导入 ${addedCount} 条连接记录`)