Skip to content

Commit 2d07f27

Browse files
committed
feat: Add quick connect group servers
1 parent 838b344 commit 2d07f27

3 files changed

Lines changed: 61 additions & 17 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,10 @@ webssh/
154154
- **断开**: 点击"断开"按钮结束连接
155155

156156
### 快速连接
157-
- 不保存服务器信息,直接输入连接参数
158-
- 适合临时连接或测试使用
157+
- **无需提前配置**:直接输入凭据即可发起 SSH / SFTP 连接
158+
- **独立多标签页架构**:每个终端和独立全屏 SFTP 文件管理器都运行在完全隔离的环境中,互不干扰
159+
- **智能历史与分组**:连接信息(包括可选保存的密码)与分组展开状态自动安全保存在当前浏览器本地中
160+
- **支持导入与导出**:一键导出 JSON 格式的快速连接数据,轻松在不同设备间迁移历史
159161

160162

161163
## 🌐 Nginx 反向代理配置

frontend/src/views/Dashboard.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140
<div class="feature-card" @click="$router.push('/quick-connect')" style="cursor: pointer;">
141141
<el-icon size="48" color="#F56C6C"><Connection /></el-icon>
142142
<h3>快速连接</h3>
143-
<p>无需保存服务器信息,快速建立SSH连接</p>
143+
<p>服务器信息保存在本地,支持导入导出</p>
144144
</div>
145145
</el-col>
146146
</el-row>

frontend/src/views/QuickConnect.vue

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@
4848
:data="treeData"
4949
:props="treeProps"
5050
node-key="id"
51-
default-expand-all
51+
:default-expanded-keys="expandedHistoryKeys"
5252
highlight-current
5353
@node-click="handleTreeNodeClick"
54+
@node-expand="handleNodeExpand"
55+
@node-collapse="handleNodeCollapse"
5456
>
5557
<template #default="{ node, data }">
5658
<div class="tree-node" :class="{ 'is-leaf': data.isLeaf }">
@@ -107,6 +109,24 @@
107109
<el-form-item label="服务器名称" prop="name">
108110
<el-input v-model="form.name" placeholder="请输入服务器名称(可选)" />
109111
</el-form-item>
112+
113+
<el-form-item label="所属分组" prop="group">
114+
<el-select
115+
v-model="form.group"
116+
filterable
117+
allow-create
118+
default-first-option
119+
placeholder="请输入或选择分组名称(可选)"
120+
style="width: 100%"
121+
>
122+
<el-option
123+
v-for="item in availableGroups"
124+
:key="item"
125+
:label="item"
126+
:value="item"
127+
/>
128+
</el-select>
129+
</el-form-item>
110130

111131
<el-form-item label="主机地址" prop="host">
112132
<el-input v-model="form.host" placeholder="请输入IP地址或域名" />
@@ -211,7 +231,6 @@ import { useTerminalStore } from '@/stores/terminal'
211231
import { useAuthStore } from '@/stores/auth'
212232
213233
const HISTORY_KEY = 'webssh_quick_connect_history'
214-
const MAX_HISTORY = 20
215234
216235
const router = useRouter()
217236
const authStore = useAuthStore()
@@ -227,6 +246,7 @@ const historyList = ref([])
227246
228247
const form = reactive({
229248
name: '',
249+
group: '',
230250
host: '',
231251
port: 22,
232252
username: '',
@@ -272,19 +292,30 @@ const rules = {
272292
]
273293
}
274294
275-
// ========== 树形数据 ==========
295+
// ========== 树形数据 & 分组下拉 ==========
296+
297+
// 提取历史记录中出现过的所有分组(去重)
298+
const availableGroups = computed(() => {
299+
const groups = new Set()
300+
historyList.value.forEach(item => {
301+
if (item.group) {
302+
groups.add(item.group)
303+
}
304+
})
305+
return Array.from(groups).sort()
306+
})
276307
277308
const treeProps = {
278309
children: 'children',
279310
label: 'label'
280311
}
281312
282-
// 将 historyList 转换为树形结构,按主机地址分组
313+
// 将 historyList 转换为树形结构,按分组名称分组
283314
const treeData = computed(() => {
284315
const groups = {}
285316
286317
historyList.value.forEach((item, index) => {
287-
const groupKey = item.host
318+
const groupKey = item.group || '未分组'
288319
if (!groups[groupKey]) {
289320
groups[groupKey] = {
290321
id: `group-${groupKey}`,
@@ -313,6 +344,24 @@ const handleTreeNodeClick = (data) => {
313344
}
314345
}
315346
347+
// ========== 分组展开状态持久化 (sessionStorage) ==========
348+
const EXPANDED_HISTORY_KEYS_KEY = 'webssh_quick_connect_expanded_keys'
349+
const expandedHistoryKeys = ref(JSON.parse(sessionStorage.getItem(EXPANDED_HISTORY_KEYS_KEY) || '[]'))
350+
351+
const handleNodeExpand = (data) => {
352+
if (!data.isLeaf && !expandedHistoryKeys.value.includes(data.id)) {
353+
expandedHistoryKeys.value.push(data.id)
354+
sessionStorage.setItem(EXPANDED_HISTORY_KEYS_KEY, JSON.stringify(expandedHistoryKeys.value))
355+
}
356+
}
357+
358+
const handleNodeCollapse = (data) => {
359+
if (!data.isLeaf) {
360+
expandedHistoryKeys.value = expandedHistoryKeys.value.filter(id => id !== data.id)
361+
sessionStorage.setItem(EXPANDED_HISTORY_KEYS_KEY, JSON.stringify(expandedHistoryKeys.value))
362+
}
363+
}
364+
316365
// ========== 历史记录管理 ==========
317366
318367
const loadHistory = () => {
@@ -331,6 +380,7 @@ const persistHistory = () => {
331380
const saveToHistory = () => {
332381
const record = {
333382
name: form.name || '',
383+
group: form.group || '',
334384
host: form.host,
335385
port: form.port,
336386
username: form.username,
@@ -356,15 +406,12 @@ const saveToHistory = () => {
356406
357407
historyList.value.unshift(record)
358408
359-
if (historyList.value.length > MAX_HISTORY) {
360-
historyList.value = historyList.value.slice(0, MAX_HISTORY)
361-
}
362-
363409
persistHistory()
364410
}
365411
366412
const fillFromHistory = (item) => {
367413
form.name = item.name || ''
414+
form.group = item.group || ''
368415
form.host = item.host
369416
form.port = item.port
370417
form.username = item.username
@@ -464,11 +511,6 @@ const handleImportFile = (event) => {
464511
merged.unshift(item)
465512
})
466513
467-
// 限制最大条数
468-
if (merged.length > MAX_HISTORY) {
469-
merged = merged.slice(0, MAX_HISTORY)
470-
}
471-
472514
historyList.value = merged
473515
persistHistory()
474516
ElMessage.success(`成功导入 ${addedCount} 条连接记录`)

0 commit comments

Comments
 (0)