feat: v0.3.0 版本 - SSH终端、SFTP、端口转发功能实现
核心功能: - SSH终端: 密码/密钥认证、XTerm.js终端、多主题配色、IP高亮 - SFTP管理: 双栏文件管理、上传下载、传输队列 - 端口转发: 本地/远程转发、SOCKS5动态代理、规则管理 架构改进: - 拆分大型组件 (SFTPView/TabBar/Sidebar) - 添加统一日志和错误处理 - 修复React严格模式下的终端黑屏问题 UI更新: - 新增端口转发视图 - 更新UI预览文档 - 左侧栏右键菜单支持SFTP快捷打开
This commit is contained in:
1 parent
cf1c054381
commit
484d850abd
105 files changed
+25330
-64
No files matched your search
@@ -0,0 +1,226 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
export type TabType = 'welcome' | 'terminal' | 'sftp' | 'portforward' | 'settings'
|
||||
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
|
||||
|
||||
// SSH 连接配置(用于快速连接)
|
||||
export interface SSHConnectionConfig {
|
||||
host: string
|
||||
port: number
|
||||
username: string
|
||||
password?: string
|
||||
privateKey?: string
|
||||
passphrase?: string
|
||||
}
|
||||
|
||||
// 连接实例信息
|
||||
export interface ConnectionInstance {
|
||||
connectionId: string // 实际的 SSH 连接 ID
|
||||
shellId?: string // Shell 会话 ID(仅 terminal 类型)
|
||||
sftpId?: string // SFTP 会话 ID(仅 sftp 类型)
|
||||
}
|
||||
|
||||
export interface Tab {
|
||||
id: string
|
||||
title: string
|
||||
type: TabType
|
||||
sessionId?: string // 持久化会话 ID(用于获取配置)
|
||||
connectionConfig?: SSHConnectionConfig // 快速连接配置(临时)
|
||||
connectionInstance?: ConnectionInstance // 实际的连接实例(每个标签独立)
|
||||
isActive: boolean
|
||||
connectionStatus?: ConnectionStatus
|
||||
}
|
||||
|
||||
interface TabState {
|
||||
tabs: Tab[]
|
||||
activeTabId: string | null
|
||||
|
||||
// Actions
|
||||
addTab: (tab: Omit<Tab, 'id' | 'isActive'>) => void
|
||||
updateTab: (tabId: string, updates: Partial<Tab>) => void
|
||||
updateTabConnectionInstance: (tabId: string, instance: ConnectionInstance) => void
|
||||
updateTabStatus: (tabId: string, status: ConnectionStatus) => void
|
||||
closeTab: (tabId: string) => Tab | null // 返回被关闭的标签(用于清理连接)
|
||||
closeOtherTabs: (tabId: string) => Tab[] // 返回被关闭的标签数组
|
||||
closeRightTabs: (tabId: string) => Tab[]
|
||||
closeLeftTabs: (tabId: string) => Tab[]
|
||||
closeAllTabs: () => Tab[]
|
||||
activateTab: (tabId: string) => void
|
||||
updateTabTitle: (tabId: string, title: string) => void
|
||||
getTabById: (tabId: string) => Tab | undefined
|
||||
}
|
||||
|
||||
export const useTabStore = create<TabState>((set, get) => ({
|
||||
tabs: [
|
||||
{ id: 'welcome', title: '欢迎', type: 'welcome', isActive: true }
|
||||
],
|
||||
activeTabId: 'welcome',
|
||||
|
||||
addTab: (tab) => {
|
||||
const { tabs } = get()
|
||||
|
||||
// 检查是否是唯一类型的标签(settings, welcome),如果有则激活而不是创建新的
|
||||
if (tab.type === 'settings' || tab.type === 'welcome') {
|
||||
const existing = tabs.find(t => t.type === tab.type)
|
||||
if (existing) {
|
||||
// 激活已存在的标签
|
||||
const updatedTabs = tabs.map(t => ({
|
||||
...t,
|
||||
isActive: t.id === existing.id,
|
||||
}))
|
||||
set({ tabs: updatedTabs, activeTabId: existing.id })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const newTab: Tab = {
|
||||
...tab,
|
||||
id: `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
isActive: true,
|
||||
}
|
||||
|
||||
// Deactivate all other tabs
|
||||
const updatedTabs = tabs.map(t => ({ ...t, isActive: false }))
|
||||
|
||||
set({
|
||||
tabs: [...updatedTabs, newTab],
|
||||
activeTabId: newTab.id,
|
||||
})
|
||||
},
|
||||
|
||||
updateTab: (tabId, updates) => {
|
||||
const { tabs } = get()
|
||||
const updatedTabs = tabs.map(t =>
|
||||
t.id === tabId ? { ...t, ...updates } : t
|
||||
)
|
||||
set({ tabs: updatedTabs })
|
||||
},
|
||||
|
||||
updateTabConnectionInstance: (tabId, instance) => {
|
||||
const { tabs } = get()
|
||||
const updatedTabs = tabs.map(t =>
|
||||
t.id === tabId ? { ...t, connectionInstance: instance } : t
|
||||
)
|
||||
set({ tabs: updatedTabs })
|
||||
},
|
||||
|
||||
updateTabStatus: (tabId, status) => {
|
||||
const { tabs } = get()
|
||||
const updatedTabs = tabs.map(t =>
|
||||
t.id === tabId ? { ...t, connectionStatus: status } : t
|
||||
)
|
||||
set({ tabs: updatedTabs })
|
||||
},
|
||||
|
||||
closeTab: (tabId) => {
|
||||
const { tabs, activeTabId } = get()
|
||||
|
||||
if (tabs.length <= 1) return null
|
||||
|
||||
const closedTab = tabs.find(t => t.id === tabId) || null
|
||||
const newTabs = tabs.filter(t => t.id !== tabId)
|
||||
|
||||
// If closing active tab, activate another one
|
||||
let newActiveId = activeTabId
|
||||
if (activeTabId === tabId) {
|
||||
const closedTabIndex = tabs.findIndex(t => t.id === tabId)
|
||||
const newActiveTab = newTabs[Math.min(closedTabIndex, newTabs.length - 1)]
|
||||
if (newActiveTab) {
|
||||
newActiveTab.isActive = true
|
||||
newActiveId = newActiveTab.id
|
||||
}
|
||||
}
|
||||
|
||||
set({ tabs: newTabs, activeTabId: newActiveId })
|
||||
return closedTab
|
||||
},
|
||||
|
||||
closeOtherTabs: (tabId) => {
|
||||
const { tabs } = get()
|
||||
const keepTab = tabs.find(t => t.id === tabId)
|
||||
if (!keepTab) return []
|
||||
|
||||
const closedTabs = tabs.filter(t => t.id !== tabId)
|
||||
keepTab.isActive = true
|
||||
set({
|
||||
tabs: [keepTab],
|
||||
activeTabId: tabId,
|
||||
})
|
||||
return closedTabs
|
||||
},
|
||||
|
||||
closeRightTabs: (tabId) => {
|
||||
const { tabs, activeTabId } = get()
|
||||
const tabIndex = tabs.findIndex(t => t.id === tabId)
|
||||
if (tabIndex === -1) return []
|
||||
|
||||
// 保留当前标签及其左侧所有标签
|
||||
const closedTabs = tabs.slice(tabIndex + 1)
|
||||
const newTabs = tabs.slice(0, tabIndex + 1)
|
||||
// 如果关闭的是当前活动标签,激活最后一个
|
||||
if (activeTabId && tabs.findIndex(t => t.id === activeTabId) > tabIndex) {
|
||||
newTabs[newTabs.length - 1].isActive = true
|
||||
set({ tabs: newTabs, activeTabId: newTabs[newTabs.length - 1].id })
|
||||
} else {
|
||||
set({ tabs: newTabs })
|
||||
}
|
||||
return closedTabs
|
||||
},
|
||||
|
||||
closeLeftTabs: (tabId) => {
|
||||
const { tabs, activeTabId } = get()
|
||||
const tabIndex = tabs.findIndex(t => t.id === tabId)
|
||||
if (tabIndex === -1) return []
|
||||
|
||||
// 保留当前标签及其右侧所有标签
|
||||
const closedTabs = tabs.slice(0, tabIndex)
|
||||
const newTabs = tabs.slice(tabIndex)
|
||||
// 如果关闭的是当前活动标签,激活第一个
|
||||
if (activeTabId && tabs.findIndex(t => t.id === activeTabId) < tabIndex) {
|
||||
newTabs[0].isActive = true
|
||||
set({ tabs: newTabs, activeTabId: newTabs[0].id })
|
||||
} else {
|
||||
set({ tabs: newTabs, activeTabId: tabId })
|
||||
}
|
||||
return closedTabs
|
||||
},
|
||||
|
||||
closeAllTabs: () => {
|
||||
const { tabs } = get()
|
||||
const welcomeTab: Tab = {
|
||||
id: 'welcome',
|
||||
title: '欢迎',
|
||||
type: 'welcome',
|
||||
isActive: true,
|
||||
}
|
||||
set({
|
||||
tabs: [welcomeTab],
|
||||
activeTabId: 'welcome',
|
||||
})
|
||||
return tabs
|
||||
},
|
||||
|
||||
activateTab: (tabId) => {
|
||||
const { tabs } = get()
|
||||
const updatedTabs = tabs.map(t => ({
|
||||
...t,
|
||||
isActive: t.id === tabId,
|
||||
}))
|
||||
set({
|
||||
tabs: updatedTabs,
|
||||
activeTabId: tabId,
|
||||
})
|
||||
},
|
||||
|
||||
updateTabTitle: (tabId, title) => {
|
||||
const { tabs } = get()
|
||||
const updatedTabs = tabs.map(t =>
|
||||
t.id === tabId ? { ...t, title } : t
|
||||
)
|
||||
set({ tabs: updatedTabs })
|
||||
},
|
||||
|
||||
getTabById: (tabId) => {
|
||||
return get().tabs.find(t => t.id === tabId)
|
||||
},
|
||||
}))
|
||||
Reference in new issue
Block a user