v2.0.0: 主题系统+SSH私钥解析修复+会话持久化+连接池优化+UI打磨
核心修复:
- ssh-service.ts: 新增 resolvePrivateKey(),将私钥文件路径(~/.ssh/id_rsa)解析为内容后传给 ssh2,
解决 'Cannot parse privateKey: Unsupported key format' 错误
- useTerminalConnection.ts + sessionHelper.ts: 根据 authType 过滤字段,password/agent 认证不再传 privateKey
- sessionStore.ts: addSession() 现在先调用 sessionCreate() 持久化到 SQLite,解决恢复标签'无可用连接信息'问题
- database-service.ts: updateSession() 补充 private_key_path 字段持久化
- SessionDialog.tsx: 默认 username='root'、privateKey='~/.ssh/id_rsa'
UI/UX:
- 语义化主题系统(app-theme.ts),强制深色模式,全局无边框设计
- macOS 标题栏支持双击最大化(window:toggleMaximize IPC)
- macOS 菜单栏('OpsXShell' 应用菜单 + Settings Cmd+,)
- WelcomeView 4卡片布局,移除 Settings 按钮
- TabBar/TabItem 背景色统一(bg-dark-800/900),消除白色边框线
架构优化:
- SFTP 生命周期修复(useSFTP.ts useRef + autoConnectAttempted guard)
- 连接池竞态修复,支持快速多标签连接
- IPC 接口规范化,安全错误处理
- Vite 代码分割优化
质量: lint零错误 | tsc零错误 | build成功 | 49 tests pass
This commit is contained in:
1 parent
37e65be823
commit
514d62644e
77 files changed
+2563
-931
No files matched your search
@@ -148,11 +148,11 @@ export default function SettingsView() {
|
||||
].map((theme) => (
|
||||
<button
|
||||
key={theme.value}
|
||||
onClick={() => setSetting('theme', theme.value as any)}
|
||||
onClick={() => setSetting('theme', theme.value as 'dark' | 'light' | 'system')}
|
||||
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'
|
||||
: 'border-dark-700/30 hover:border-dark-600 text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{theme.label}
|
||||
@@ -165,8 +165,8 @@ export default function SettingsView() {
|
||||
<SettingSectionTitle>语言</SettingSectionTitle>
|
||||
<select
|
||||
value={settings.language}
|
||||
onChange={(e) => setSetting('language', e.target.value as any)}
|
||||
className="w-full mt-3 px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
onChange={(e) => setSetting('language', e.target.value as 'zh-CN' | 'en-US')}
|
||||
className="w-full mt-3 px-3 py-2 bg-dark-900 border border-dark-700/30 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
>
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="en-US">English</option>
|
||||
@@ -187,7 +187,7 @@ export default function SettingsView() {
|
||||
<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-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
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"
|
||||
>
|
||||
<option>JetBrains Mono</option>
|
||||
<option>Fira Code</option>
|
||||
@@ -200,7 +200,7 @@ export default function SettingsView() {
|
||||
<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-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
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"
|
||||
>
|
||||
{[12, 13, 14, 15, 16, 18, 20].map((size) => (
|
||||
<option key={size} value={size}>{size}px</option>
|
||||
@@ -216,11 +216,11 @@ export default function SettingsView() {
|
||||
{Object.entries(themes).map(([key, theme]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setSetting('terminalTheme', key as any)}
|
||||
onClick={() => setSetting('terminalTheme', key as 'dracula' | 'monokai' | 'solarized' | 'one-dark')}
|
||||
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'
|
||||
: 'border-dark-700/30 hover:border-dark-600 text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
@@ -240,7 +240,7 @@ export default function SettingsView() {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<SettingCard>
|
||||
<div className="overflow-hidden rounded-lg border border-dark-700">
|
||||
<div className="overflow-hidden rounded-lg border border-dark-700/30">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-dark-800/60 text-gray-400 text-left">
|
||||
@@ -253,7 +253,7 @@ export default function SettingsView() {
|
||||
{SHORTCUTS.map((s, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className="border-t border-dark-700/60 hover:bg-dark-800/40 transition-colors"
|
||||
className="border-t border-dark-700/20 hover:bg-dark-800/40 transition-colors"
|
||||
>
|
||||
<td className="px-4 py-2.5 text-gray-200">{s.action}</td>
|
||||
<td className="px-4 py-2.5">
|
||||
@@ -293,7 +293,7 @@ export default function SettingsView() {
|
||||
type="number"
|
||||
value={settings.defaultPort}
|
||||
onChange={(e) => setSetting('defaultPort', parseInt(e.target.value) || 22)}
|
||||
className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
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"
|
||||
/>
|
||||
</SettingCard>
|
||||
|
||||
@@ -303,7 +303,7 @@ export default function SettingsView() {
|
||||
type="number"
|
||||
value={settings.timeout}
|
||||
onChange={(e) => setSetting('timeout', parseInt(e.target.value) || 30)}
|
||||
className="w-32 mt-2 px-3 py-2 bg-dark-900 border border-dark-700 rounded-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
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"
|
||||
/>
|
||||
</SettingCard>
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function SettingsView() {
|
||||
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-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
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"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">最大并发 SSH 连接数</div>
|
||||
</div>
|
||||
@@ -337,7 +337,7 @@ export default function SettingsView() {
|
||||
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-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
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"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">非活跃标签保持连接数</div>
|
||||
</div>
|
||||
@@ -352,7 +352,7 @@ export default function SettingsView() {
|
||||
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-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
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"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">无操作后进入挂起</div>
|
||||
</div>
|
||||
@@ -364,7 +364,7 @@ export default function SettingsView() {
|
||||
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-lg text-gray-300 focus:outline-none focus:border-accent-blue"
|
||||
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"
|
||||
/>
|
||||
<div className="text-xs text-gray-600 mt-1">挂起后自动断开</div>
|
||||
</div>
|
||||
@@ -442,7 +442,7 @@ export default function SettingsView() {
|
||||
['Tailwind CSS', '3.3+'],
|
||||
['Lucide React', '0.294+'],
|
||||
].map(([name, ver]) => (
|
||||
<div key={name} className="flex justify-between py-1 border-b border-dark-700/40">
|
||||
<div key={name} className="flex justify-between py-1 border-b border-dark-700/20">
|
||||
<span className="text-gray-400">{name}</span>
|
||||
<span className="text-gray-500 font-mono">{ver}</span>
|
||||
</div>
|
||||
@@ -475,9 +475,9 @@ export default function SettingsView() {
|
||||
return (
|
||||
<div className="h-full flex bg-dark-900">
|
||||
{/* ── 左侧导航 ── */}
|
||||
<div className="w-56 flex-shrink-0 bg-dark-800/50 border-r border-dark-700 flex flex-col">
|
||||
<div className="w-56 flex-shrink-0 bg-dark-800 flex flex-col">
|
||||
{/* 搜索框 */}
|
||||
<div className="p-3 border-b border-dark-700">
|
||||
<div className="p-3 border-b border-dark-700/20">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-500" />
|
||||
<input
|
||||
@@ -485,7 +485,7 @@ export default function SettingsView() {
|
||||
placeholder="搜索设置..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-8 pr-8 py-1.5 text-sm bg-dark-900 border border-dark-700 rounded-lg text-gray-300 placeholder-gray-600 focus:outline-none focus:border-accent-blue/60"
|
||||
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 && (
|
||||
<button
|
||||
@@ -507,16 +507,16 @@ export default function SettingsView() {
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors ${
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-all ${
|
||||
active
|
||||
? 'bg-accent-blue/15 text-white'
|
||||
: 'text-gray-400 hover:bg-dark-700/60 hover:text-gray-200'
|
||||
? 'bg-accent-blue text-white shadow-sm'
|
||||
: 'text-gray-400 hover:bg-dark-700/50 hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Icon className={`w-4 h-4 flex-shrink-0 ${active ? 'text-accent-blue' : ''}`} />
|
||||
<Icon className={`w-4 h-4 flex-shrink-0 ${active ? 'text-white' : ''}`} />
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium truncate">{tab.label}</div>
|
||||
<div className={`text-xs truncate ${active ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||
<div className={`text-xs truncate ${active ? 'text-white/70' : 'text-gray-600'}`}>
|
||||
{tab.description}
|
||||
</div>
|
||||
</div>
|
||||
@@ -529,7 +529,7 @@ export default function SettingsView() {
|
||||
{/* ── 右侧内容区 ── */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Header bar */}
|
||||
<div className="flex-shrink-0 px-8 pt-6 pb-4 border-b border-dark-700/50">
|
||||
<div className="flex-shrink-0 px-8 pt-6 pb-4 border-b border-dark-700/20">
|
||||
<h1 className="text-xl font-semibold text-gray-100">{currentMeta.label}</h1>
|
||||
<p className="text-sm text-gray-500 mt-0.5">{currentMeta.description}</p>
|
||||
</div>
|
||||
@@ -558,7 +558,7 @@ function SettingCard({
|
||||
className={`rounded-xl p-5 ${
|
||||
highlight
|
||||
? 'bg-dark-800/80 border border-accent-blue/20'
|
||||
: 'bg-dark-800/50 border border-dark-700/50'
|
||||
: 'bg-dark-800/50 border border-dark-700/30'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
|
||||
Reference in new issue
Block a user