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,394 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
import {
|
||||
Monitor, Palette, Keyboard, Shield, Database, Terminal
|
||||
} from 'lucide-react'
|
||||
import { themes } from '../themes/terminal'
|
||||
|
||||
type SettingsTab = 'general' | 'appearance' | 'terminal' | 'ssh' | 'security' | 'about'
|
||||
|
||||
export default function SettingsView() {
|
||||
const { settings, isLoaded, loadSettings, setSetting } = useSettingsStore()
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>('general')
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings()
|
||||
}, [])
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general' as SettingsTab, label: '常规', icon: Monitor },
|
||||
{ id: 'appearance' as SettingsTab, label: '外观', icon: Palette },
|
||||
{ id: 'terminal' as SettingsTab, label: '终端', icon: Keyboard },
|
||||
{ id: 'ssh' as SettingsTab, label: 'SSH', icon: Shield },
|
||||
{ id: 'security' as SettingsTab, label: '安全', icon: Database },
|
||||
{ id: 'about' as SettingsTab, label: '关于', icon: Monitor },
|
||||
]
|
||||
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center bg-dark-900">
|
||||
<div className="text-gray-400">加载中...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'general':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold text-gray-200">常规设置</h2>
|
||||
<div className="space-y-4">
|
||||
<SettingToggle
|
||||
label="启动时恢复会话"
|
||||
description="应用启动时自动恢复上次的会话"
|
||||
checked={settings.restoreOnStart}
|
||||
onChange={(v) => setSetting('restoreOnStart', v)}
|
||||
/>
|
||||
<SettingToggle
|
||||
label="自动保存会话"
|
||||
description="自动保存会话配置更改"
|
||||
checked={settings.autoSave}
|
||||
onChange={(v) => setSetting('autoSave', v)}
|
||||
/>
|
||||
<SettingToggle
|
||||
label="显示通知"
|
||||
description="在桌面显示连接状态通知"
|
||||
checked={settings.showNotifications}
|
||||
onChange={(v) => setSetting('showNotifications', v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'appearance':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold text-gray-200">外观设置</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-dark-800 rounded-lg">
|
||||
<div className="font-medium text-gray-200 mb-3">主题</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ value: 'dark', label: '深色' },
|
||||
{ value: 'light', label: '浅色' },
|
||||
{ value: 'system', label: '跟随系统' },
|
||||
].map((theme) => (
|
||||
<button
|
||||
key={theme.value}
|
||||
onClick={() => setSetting('theme', theme.value as any)}
|
||||
className={`p-3 rounded-lg border text-sm transition-colors ${
|
||||
settings.theme === theme.value
|
||||
? 'border-accent-blue bg-accent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700 hover:border-dark-600 text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{theme.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-dark-800 rounded-lg">
|
||||
<div className="font-medium text-gray-200 mb-3">语言</div>
|
||||
<select
|
||||
value={settings.language}
|
||||
onChange={(e) => setSetting('language', e.target.value as any)}
|
||||
className="w-full px-3 py-2 bg-dark-900 border border-dark-700 rounded text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
>
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="en-US">English</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'terminal':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold text-gray-200">终端设置</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-dark-800 rounded-lg">
|
||||
<div className="font-medium text-gray-200 mb-3">字体</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">字体族</label>
|
||||
<select
|
||||
value={settings.fontFamily}
|
||||
onChange={(e) => setSetting('fontFamily', e.target.value)}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700 rounded text-gray-300"
|
||||
>
|
||||
<option>JetBrains Mono</option>
|
||||
<option>Fira Code</option>
|
||||
<option>Consolas</option>
|
||||
<option>Monaco</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">字体大小</label>
|
||||
<select
|
||||
value={settings.fontSize}
|
||||
onChange={(e) => setSetting('fontSize', parseInt(e.target.value))}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700 rounded text-gray-300"
|
||||
>
|
||||
{[12, 13, 14, 15, 16, 18, 20].map((size) => (
|
||||
<option key={size} value={size}>{size}px</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-dark-800 rounded-lg">
|
||||
<div className="font-medium text-gray-200 mb-3">终端配色</div>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{Object.entries(themes).map(([key, theme]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setSetting('terminalTheme', key as any)}
|
||||
className={`p-3 rounded-lg border text-sm transition-colors ${
|
||||
settings.terminalTheme === key
|
||||
? 'border-accent-blue bg-accent-blue/20 text-accent-blue'
|
||||
: 'border-dark-700 hover:border-dark-600 text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="w-full h-6 rounded mb-1"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
/>
|
||||
{theme.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'ssh':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold text-gray-200">SSH 设置</h2>
|
||||
<div className="space-y-4">
|
||||
<SettingToggle
|
||||
label="保持连接活跃"
|
||||
description="定期发送心跳包保持连接"
|
||||
checked={settings.keepAlive}
|
||||
onChange={(v) => setSetting('keepAlive', v)}
|
||||
/>
|
||||
|
||||
<div className="p-4 bg-dark-800 rounded-lg">
|
||||
<div className="font-medium text-gray-200 mb-3">默认端口</div>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.defaultPort}
|
||||
onChange={(e) => setSetting('defaultPort', parseInt(e.target.value) || 22)}
|
||||
className="w-32 px-3 py-2 bg-dark-900 border border-dark-700 rounded text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-dark-800 rounded-lg">
|
||||
<div className="font-medium text-gray-200 mb-3">超时设置 (秒)</div>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.timeout}
|
||||
onChange={(e) => setSetting('timeout', parseInt(e.target.value) || 30)}
|
||||
className="w-32 px-3 py-2 bg-dark-900 border border-dark-700 rounded text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 专业连接管理设置 */}
|
||||
<div className="p-4 bg-dark-800 rounded-lg border border-accent-blue/30">
|
||||
<div className="font-medium text-gray-200 mb-2 flex items-center gap-2">
|
||||
<span className="text-accent-blue">专业版</span>
|
||||
连接管理
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
配置连接池行为,优化资源使用和性能
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">最大连接数</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
value={settings.maxConnections || 10}
|
||||
onChange={(e) => setSetting('maxConnections', parseInt(e.target.value) || 10)}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700 rounded text-gray-300"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">最大并发 SSH 连接数</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">最大后台连接</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
value={settings.maxBackgroundConnections || 5}
|
||||
onChange={(e) => setSetting('maxBackgroundConnections', parseInt(e.target.value) || 5)}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700 rounded text-gray-300"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">非活跃标签保持连接数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">空闲超时 (分钟)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={120}
|
||||
value={Math.floor((settings.idleTimeoutMs || 5 * 60 * 1000) / 60000)}
|
||||
onChange={(e) => setSetting('idleTimeoutMs', (parseInt(e.target.value) || 5) * 60000)}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700 rounded text-gray-300"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">无操作后进入挂起</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-500">挂起超时 (分钟)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={240}
|
||||
value={Math.floor((settings.suspendTimeoutMs || 30 * 60 * 1000) / 60000)}
|
||||
onChange={(e) => setSetting('suspendTimeoutMs', (parseInt(e.target.value) || 30) * 60000)}
|
||||
className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700 rounded text-gray-300"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">挂起后自动断开</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingToggle
|
||||
label="启用终端快照"
|
||||
description="切换标签时保存终端内容,快速恢复显示"
|
||||
checked={settings.enableSnapshots !== false}
|
||||
onChange={(v) => setSetting('enableSnapshots', v)}
|
||||
/>
|
||||
|
||||
<SettingToggle
|
||||
label="自动重连"
|
||||
description="连接断开后自动尝试重连(最多3次)"
|
||||
checked={settings.autoReconnect !== false}
|
||||
onChange={(v) => setSetting('autoReconnect', v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'security':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold text-gray-200">安全设置</h2>
|
||||
<div className="space-y-4">
|
||||
<SettingToggle
|
||||
label="加密存储密码"
|
||||
description="使用 AES-256 加密存储密码"
|
||||
checked={settings.encryptPasswords}
|
||||
onChange={(v) => setSetting('encryptPasswords', v)}
|
||||
/>
|
||||
|
||||
<SettingToggle
|
||||
label="锁定屏幕时断开连接"
|
||||
description="系统锁屏时自动断开 SSH 连接"
|
||||
checked={settings.lockOnSleep}
|
||||
onChange={(v) => setSetting('lockOnSleep', v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'about':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold text-gray-200">关于 OpsXShell</h2>
|
||||
<div className="p-6 bg-dark-800 rounded-lg text-center">
|
||||
<div className="w-20 h-20 bg-dark-700 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<Terminal className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-200 mb-1">OpsXShell</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">版本 1.0.0</p>
|
||||
<p className="text-sm text-gray-400 max-w-md mx-auto">
|
||||
跨平台 SSH/SFTP 终端工具 · 简洁高效 · 安全可靠 · 操作便捷人性化
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex bg-dark-900">
|
||||
{/* Sidebar */}
|
||||
<div className="w-48 bg-dark-800 border-r border-dark-700 p-4">
|
||||
<nav className="space-y-1">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-accent-blue text-white'
|
||||
: 'text-gray-400 hover:bg-dark-700 hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 p-8 overflow-y-auto">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 设置开关组件
|
||||
function SettingToggle({
|
||||
label,
|
||||
description,
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
label: string
|
||||
description: string
|
||||
checked: boolean
|
||||
onChange: (value: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 bg-dark-800 rounded-lg">
|
||||
<div>
|
||||
<div className="font-medium text-gray-200">{label}</div>
|
||||
<div className="text-sm text-gray-500">{description}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors ${
|
||||
checked ? 'bg-accent-blue' : 'bg-dark-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${
|
||||
checked ? 'left-7' : 'left-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user