import { useEffect, useState, useMemo } from 'react' import { useSettingsStore } from '../stores/settingsStore' import { Monitor, Palette, Terminal, Keyboard, Shield, Database, Info, Search, X, } from 'lucide-react' import { themes } from '../themes/terminal' // ─── Tab 类型 ─────────────────────────────────────────────── type SettingsTab = 'general' | 'appearance' | 'terminal' | 'shortcuts' | 'ssh' | 'security' | 'about' interface TabMeta { id: SettingsTab label: string description: string icon: React.ComponentType<{ className?: string }> } const TAB_LIST: TabMeta[] = [ { id: 'general', label: '常规', description: '应用启动与通知', icon: Monitor }, { id: 'appearance', label: '外观', description: '主题与语言偏好', icon: Palette }, { id: 'terminal', label: '终端', description: '字体与配色方案', icon: Terminal }, { id: 'shortcuts', label: '快捷键', description: '键盘快捷键管理', icon: Keyboard }, { id: 'ssh', label: 'SSH', description: '连接与认证设置', icon: Shield }, { id: 'security', label: '安全', description: '加密与隐私设置', icon: Database }, { id: 'about', label: '关于', description: '版本与许可信息', icon: Info }, ] // ─── 快捷键数据 ───────────────────────────────────────────── interface ShortcutEntry { action: string keys: string category: string } const SHORTCUTS: ShortcutEntry[] = [ { action: '切换到下一个标签', keys: 'Ctrl+Tab', category: '标签管理' }, { action: '切换到上一个标签', keys: 'Ctrl+Shift+Tab', category: '标签管理' }, { action: '跳转到第 N 个标签', keys: 'Ctrl+1 … Ctrl+9', category: '标签管理' }, { action: '关闭当前标签', keys: 'Ctrl+W', category: '标签管理' }, { action: '新建标签', keys: 'Ctrl+Shift+N', category: '标签管理' }, { action: '切换全屏', keys: 'F11', category: '视图' }, { action: '新建会话', keys: 'Ctrl+N', category: '会话' }, { action: '快速连接', keys: 'Ctrl+K', category: '会话' }, { action: '命令面板', keys: 'Ctrl+Shift+P', category: '工具' }, ] // ─── 主组件 ───────────────────────────────────────────────── export default function SettingsView() { const { settings, isLoaded, loadSettings, setSetting } = useSettingsStore() const [activeTab, setActiveTab] = useState('general') const [searchQuery, setSearchQuery] = useState('') const [appVersion, setAppVersion] = useState('—') useEffect(() => { loadSettings() }, []) // eslint-disable-line react-hooks/exhaustive-deps // 获取版本号(参考 StatusBar 实现) useEffect(() => { if (window.electronAPI) { window.electronAPI.getVersion().then((v: string) => setAppVersion(v)) } }, []) // 当前 tab 元信息 const currentMeta = useMemo(() => TAB_LIST.find((t) => t.id === activeTab)!, [activeTab]) // ── 搜索过滤 ────────────────────────────────────────────── const filtered = useMemo((): SettingsTab[] | null => { if (!searchQuery.trim()) return null // null 表示不过滤,正常显示 const q = searchQuery.toLowerCase() const keywords: Record = { general: ['启动', '恢复', '会话', '自动保存', '通知', 'restore', 'notification'], appearance: ['主题', '深色', '浅色', '语言', '中文', 'english', 'theme', 'language'], terminal: ['字体', 'font', '配色', 'terminal', 'monokai', 'dracula'], shortcuts: ['快捷键', '键盘', 'keyboard', 'shortcut', 'hotkey'], ssh: ['ssh', '连接', '端口', '超时', '心跳', '连接池', '重连', '快照'], security: ['加密', '安全', '密码', '锁屏', 'encrypt', 'lock'], about: ['版本', '关于', '许可', 'version', 'about', 'license'], } const matchIds = (Object.entries(keywords) as [SettingsTab, string[]][]) .filter(([, kws]) => kws.some((k: string) => k.includes(q) || q.includes(k))) .map(([id]) => id) return matchIds.length > 0 ? matchIds : [] }, [searchQuery]) // 如果搜索过滤后有结果,自动跳转到第一个匹配 tab useEffect(() => { if (filtered && filtered.length > 0 && !filtered.includes(activeTab)) { setActiveTab(filtered[0]) } }, [filtered]) // eslint-disable-line react-hooks/exhaustive-deps const visibleTabs = filtered ? TAB_LIST.filter((t) => filtered.includes(t.id)) : TAB_LIST // ── 加载态 ──────────────────────────────────────────────── if (!isLoaded) { return (
加载中...
) } // ── 渲染内容区 ──────────────────────────────────────────── const renderContent = () => { switch (activeTab) { // ──────── 常规 ───────── case 'general': return (
setSetting('restoreOnStart', v)} /> setSetting('autoSave', v)} /> setSetting('showNotifications', v)} />
) // ──────── 外观 ───────── case 'appearance': return (
主题
{[ { value: 'dark', label: '深色' }, { value: 'light', label: '浅色' }, { value: 'system', label: '跟随系统' }, ].map((theme) => ( ))}
语言
) // ──────── 终端 ───────── case 'terminal': return (
字体
终端配色
{Object.entries(themes).map(([key, theme]) => ( ))}
) // ──────── 快捷键 ─────── case 'shortcuts': return (
{SHORTCUTS.map((s, i) => ( ))}
操作 快捷键 分类
{s.action} {s.keys} {s.category}

快捷键自定义功能将在后续版本中支持。

) // ──────── SSH ────────── case 'ssh': return (
setSetting('keepAlive', v)} /> 默认端口 setSetting('defaultPort', parseInt(e.target.value) || 22)} className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue" /> 超时设置 (秒) setSetting('timeout', parseInt(e.target.value) || 30)} className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue" /> {/* 专业连接管理 */}
Pro 连接管理

配置连接池行为,优化资源使用和性能

setSetting('maxConnections', parseInt(e.target.value) || 10)} className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue" />
最大并发 SSH 连接数
setSetting('maxBackgroundConnections', parseInt(e.target.value) || 5)} className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue" />
非活跃标签保持连接数
setSetting('idleTimeoutMs', (parseInt(e.target.value) || 5) * 60000)} className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue" />
无操作后进入挂起
setSetting('suspendTimeoutMs', (parseInt(e.target.value) || 30) * 60000)} className="w-full mt-1 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue" />
挂起后自动断开
setSetting('enableSnapshots', v)} /> setSetting('autoReconnect', v)} />
) // ──────── 安全 ───────── case 'security': return (
setSetting('encryptPasswords', v)} /> setSetting('lockOnSleep', v)} />
) // ──────── 关于 ───────── case 'about': return (

OpsXShell

版本 {appVersion}

跨平台 SSH/SFTP 终端工具 · 简洁高效 · 安全可靠 · 操作便捷人性化

技术栈
{[ ['Electron', '28+'], ['React', '18'], ['TypeScript', '5'], ['Vite', '5'], ['Zustand', '4.4+'], ['XTerm.js', '5.5+'], ['Tailwind CSS', '3.3+'], ['Lucide React', '0.294+'], ].map(([name, ver]) => (
{name} {ver}
))}
开源许可

OpsXShell 基于 MIT 许可证开源。本项目使用了以下开源组件:

  • ssh2 — MIT License
  • xterm.js — MIT License
  • zmodem-ts — MIT License
  • better-sqlite3 — MIT License
  • lucide-react — ISC License
) default: return null } } // ── 主布局 ──────────────────────────────────────────────── return (
{/* ── 左侧导航 ── */}
{/* 搜索框 */}
setSearchQuery(e.target.value)} className="w-full pl-8 pr-8 py-1.5 text-sm bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 placeholder-gray-600 focus:outline-none focus:border-accent-blue/60" /> {searchQuery && ( )}
{/* Tab 列表 */}
{/* ── 右侧内容区 ── */}
{/* Header bar */}

{currentMeta.label}

{currentMeta.description}

{/* 内容 */}
{renderContent()}
) } // ─── 子组件 ───────────────────────────────────────────────── /** 卡片容器 */ function SettingCard({ children, highlight = false, }: { children: React.ReactNode highlight?: boolean }) { return (
{children}
) } /** 区域小标题 */ function SettingSectionTitle({ children, className = '', }: { children: React.ReactNode className?: string }) { return (
{children}
) } /** 开关设置项 */ function SettingToggle({ label, description, checked, onChange, }: { label: string description: string checked: boolean onChange: (value: boolean) => void }) { return (
{label}
{description}
) }