/** * PortForwardView - 端口转发管理视图 * * 支持本地端口转发、远程端口转发和动态 SOCKS5 代理 */ import { useState, useEffect } from 'react' import { useTabStore } from '../stores/tabStore' import { useConnectionPool } from '../stores/connection' import { ArrowRightLeft, Globe, Server, Plus, Trash2, Play, Square } from 'lucide-react' import { useConfirmDialog } from './common/ConfirmDialog' interface PortForwardViewProps { tabId: string isActive?: boolean } interface ForwardRule { id: string type: 'local' | 'remote' | 'dynamic' name: string localPort: number remoteHost?: string remotePort?: number isActive: boolean } export default function PortForwardView({ tabId, isActive: _isActive = true }: PortForwardViewProps) { const { tabs, updateTab } = useTabStore() const pool = useConnectionPool() const { confirm, ConfirmDialogComponent } = useConfirmDialog() const currentTab = tabs.find(t => t.id === tabId) const needsReconnect = currentTab?.needsReconnect const connectionId = currentTab?.connectionInstance?.connectionId const connection = connectionId ? pool.getConnection(connectionId) : null const [rules, setRules] = useState([]) const [showAddModal, setShowAddModal] = useState(false) const [newRule, setNewRule] = useState>({ type: 'local', name: '', localPort: 0, remoteHost: '', remotePort: 0, }) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) // Load saved rules from settings useEffect(() => { if (connectionId) { window.electronAPI?.settingsGet(`portForwards_${connectionId}`).then((result) => { if (result.success && result.value) { setRules(JSON.parse(result.value as string)) } }) } }, [connectionId]) // Save rules const saveRules = (newRules: ForwardRule[]) => { setRules(newRules) if (connectionId) { window.electronAPI?.settingsSet(`portForwards_${connectionId}`, JSON.stringify(newRules)) } } // Add new rule const handleAddRule = async () => { if (!newRule.name || !newRule.localPort) { setError('请填写完整信息') return } if (newRule.type !== 'dynamic' && (!newRule.remoteHost || !newRule.remotePort)) { setError('请填写远程主机和端口') return } const rule: ForwardRule = { id: `pf-${Date.now()}`, type: newRule.type as 'local' | 'remote' | 'dynamic', name: newRule.name, localPort: newRule.localPort, remoteHost: newRule.remoteHost, remotePort: newRule.remotePort, isActive: false, } saveRules([...rules, rule]) setShowAddModal(false) setNewRule({ type: 'local', name: '', localPort: 0, remoteHost: '', remotePort: 0 }) setError(null) } // Start port forward const startForward = async (rule: ForwardRule) => { if (!connectionId) { setError('未连接到服务器') return } setIsLoading(true) setError(null) try { let result if (rule.type === 'local') { result = await window.electronAPI?.sshForwardLocal( connectionId, rule.localPort, rule.remoteHost || '', rule.remotePort || 0 ) } else if (rule.type === 'remote') { result = await window.electronAPI?.sshForwardRemote( connectionId, rule.remotePort || 0, '127.0.0.1', rule.localPort ) } else if (rule.type === 'dynamic') { result = await window.electronAPI?.sshForwardDynamic(connectionId, rule.localPort) } if (result?.success) { const updatedRules = rules.map(r => r.id === rule.id ? { ...r, isActive: true } : r ) saveRules(updatedRules) } else { setError(result?.error || '启动失败') } } catch (err: unknown) { setError((err instanceof Error ? err.message : String(err)) || '启动失败') } finally { setIsLoading(false) } } // Stop port forward const stopForward = async (rule: ForwardRule) => { // Note: Actual stop implementation would require storing server references const updatedRules = rules.map(r => r.id === rule.id ? { ...r, isActive: false } : r ) saveRules(updatedRules) } // Delete rule const deleteRule = async (ruleId: string) => { const confirmed = await confirm({ title: '确认删除', message: '确定删除此转发规则?', danger: true, confirmText: '删除', }) if (confirmed) { saveRules(rules.filter(r => r.id !== ruleId)) } } return ( <>
{/* 会话恢复 - 需要重新连接覆盖层 */} {needsReconnect && currentTab?.sessionId && (
会话已恢复
上次关闭时的端口转发标签已恢复
需要重新建立 SSH 连接才能使用
)} {/* Header */}

端口转发

{connection ? `已连接: ${connection.config.username}@${connection.config.host}` : '未连接 - 请先建立 SSH 连接'}

{/* Rules List */}
{rules.length === 0 ? (

暂无端口转发规则

点击右上角添加规则

) : (
{rules.map((rule) => (
{/* Type Icon */}
{rule.type === 'local' && } {rule.type === 'remote' && } {rule.type === 'dynamic' && }
{/* Rule Info */}
{rule.name} {rule.type === 'local' ? '本地转发' : rule.type === 'remote' ? '远程转发' : '动态 SOCKS5'} {rule.isActive && ( 运行中 )}
{rule.type === 'dynamic' ? ( SOCKS5 代理: 127.0.0.1:{rule.localPort} ) : rule.type === 'local' ? ( 127.0.0.1:{rule.localPort} → {rule.remoteHost}:{rule.remotePort} ) : ( 远程 {rule.remotePort} → 本地 {rule.localPort} )}
{/* Actions */}
{rule.isActive ? ( ) : ( )}
))}
)}
{/* Error Message */} {error && (

{error}

)} {/* Add Rule Modal */} {showAddModal && (

添加端口转发规则

{/* Type Selection */}
{[ { type: 'local', label: '本地转发', desc: '本地→远程' }, { type: 'remote', label: '远程转发', desc: '远程→本地' }, { type: 'dynamic', label: '动态 SOCKS5', desc: '代理服务器' }, ].map((t) => ( ))}
{/* Form Fields */}
setNewRule({ ...newRule, name: e.target.value })} placeholder="例如: MySQL 转发" className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue" />
setNewRule({ ...newRule, localPort: parseInt(e.target.value) })} placeholder="例如: 3306" className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue" />
{newRule.type !== 'dynamic' && (
setNewRule({ ...newRule, remotePort: parseInt(e.target.value) })} placeholder="例如: 3306" className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue" />
)}
{newRule.type === 'local' && (
setNewRule({ ...newRule, remoteHost: e.target.value })} placeholder="例如: localhost 或 192.168.1.100" className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-content-primary placeholder-ph focus:outline-none focus:border-accent-blue" />
)}
{/* Actions */}
)}
{ConfirmDialogComponent} ) }