feat: Add quick connect group servers
This commit is contained in:
1 parent
838b344219
commit
2d07f270ae
3 files changed
+61
-17
No files matched your search
@@ -154,8 +154,10 @@ webssh/
|
|||||||
- **断开**: 点击"断开"按钮结束连接
|
- **断开**: 点击"断开"按钮结束连接
|
||||||
|
|
||||||
### 快速连接
|
### 快速连接
|
||||||
- 不保存服务器信息,直接输入连接参数
|
- **无需提前配置**:直接输入凭据即可发起 SSH / SFTP 连接
|
||||||
- 适合临时连接或测试使用
|
- **独立多标签页架构**:每个终端和独立全屏 SFTP 文件管理器都运行在完全隔离的环境中,互不干扰
|
||||||
|
- **智能历史与分组**:连接信息(包括可选保存的密码)与分组展开状态自动安全保存在当前浏览器本地中
|
||||||
|
- **支持导入与导出**:一键导出 JSON 格式的快速连接数据,轻松在不同设备间迁移历史
|
||||||
|
|
||||||
|
|
||||||
## 🌐 Nginx 反向代理配置
|
## 🌐 Nginx 反向代理配置
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} 条连接记录`)
|
||||||
|
|||||||
Reference in new issue
Block a user