diff --git a/README.md b/README.md
index fe05a6e..b0ee3c1 100644
--- a/README.md
+++ b/README.md
@@ -154,8 +154,10 @@ webssh/
- **断开**: 点击"断开"按钮结束连接
### 快速连接
-- 不保存服务器信息,直接输入连接参数
-- 适合临时连接或测试使用
+- **无需提前配置**:直接输入凭据即可发起 SSH / SFTP 连接
+- **独立多标签页架构**:每个终端和独立全屏 SFTP 文件管理器都运行在完全隔离的环境中,互不干扰
+- **智能历史与分组**:连接信息(包括可选保存的密码)与分组展开状态自动安全保存在当前浏览器本地中
+- **支持导入与导出**:一键导出 JSON 格式的快速连接数据,轻松在不同设备间迁移历史
## 🌐 Nginx 反向代理配置
diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue
index c18db91..2f3bc54 100644
--- a/frontend/src/views/Dashboard.vue
+++ b/frontend/src/views/Dashboard.vue
@@ -140,7 +140,7 @@
@@ -107,6 +109,24 @@
+
+
+
+
+
+
@@ -211,7 +231,6 @@ import { useTerminalStore } from '@/stores/terminal'
import { useAuthStore } from '@/stores/auth'
const HISTORY_KEY = 'webssh_quick_connect_history'
-const MAX_HISTORY = 20
const router = useRouter()
const authStore = useAuthStore()
@@ -227,6 +246,7 @@ const historyList = ref([])
const form = reactive({
name: '',
+ group: '',
host: '',
port: 22,
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 = {
children: 'children',
label: 'label'
}
-// 将 historyList 转换为树形结构,按主机地址分组
+// 将 historyList 转换为树形结构,按分组名称分组
const treeData = computed(() => {
const groups = {}
historyList.value.forEach((item, index) => {
- const groupKey = item.host
+ const groupKey = item.group || '未分组'
if (!groups[groupKey]) {
groups[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 = () => {
@@ -331,6 +380,7 @@ const persistHistory = () => {
const saveToHistory = () => {
const record = {
name: form.name || '',
+ group: form.group || '',
host: form.host,
port: form.port,
username: form.username,
@@ -356,15 +406,12 @@ const saveToHistory = () => {
historyList.value.unshift(record)
- if (historyList.value.length > MAX_HISTORY) {
- historyList.value = historyList.value.slice(0, MAX_HISTORY)
- }
-
persistHistory()
}
const fillFromHistory = (item) => {
form.name = item.name || ''
+ form.group = item.group || ''
form.host = item.host
form.port = item.port
form.username = item.username
@@ -464,11 +511,6 @@ const handleImportFile = (event) => {
merged.unshift(item)
})
- // 限制最大条数
- if (merged.length > MAX_HISTORY) {
- merged = merged.slice(0, MAX_HISTORY)
- }
-
historyList.value = merged
persistHistory()
ElMessage.success(`成功导入 ${addedCount} 条连接记录`)