feat(i18n): add multi-language support (ja/ko/vi/ru/zh-TW) with system auto-detection
- Extend AppLanguage to 7 locales: zh-CN, zh-TW, en, ja, ko, vi, ru - Auto-detect system language via navigator.languages; fallback to English - Add BCP47 locale mapping for date formatting and sorting - Create 50 translation JSON files (5 languages × 10 namespaces, 1137 keys each) - Update language selector with native language names - Extend locale key parity test to cover all 7 languages (70 assertions) - Add VNC/RDP desktop, serial terminal, AI agent, port forwarding, and SFTP features - Add credential encryption, host metrics, session backup crypto, and zmodem bridge - Update Rust backend: desktop services, host metrics, SSH/SFTP enhancements
This commit is contained in:
1 parent
fea6d8db04
commit
b0d0e3a5f3
301 files changed
+28002
-2705
No files matched your search
@@ -1,4 +1,6 @@
|
||||
import { Plus, Zap, Clock, Server, ChevronRight, Laptop, Terminal, Monitor, Cast } from 'lucide-react'
|
||||
import { Plus, LogIn, Clock, Server, ChevronRight, Laptop, Terminal, Monitor, Cast } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import HostDistroIcon from '../common/HostDistroIcon'
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from 'react'
|
||||
import { useSessionStore } from '../../stores'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
@@ -8,10 +10,13 @@ import { openLocalShellTab, type ShellOption } from '../layout/sidebar/LocalShel
|
||||
import appIconUrl from '../../assets/opsxshell-app-icon.png'
|
||||
import { shellDisplayFromPath } from '../../utils/shellName'
|
||||
import { fallbackLocalShells, modKeyLabel } from '../../utils/platformShells'
|
||||
import { buildOpenTabPayload } from '../../utils/sessionProtocol'
|
||||
|
||||
interface WelcomeViewProps {
|
||||
onNewSession?: () => void
|
||||
onQuickConnect?: () => void
|
||||
onRdpConnect?: () => void
|
||||
onVncConnect?: () => void
|
||||
}
|
||||
|
||||
function Kbd({ children }: { children: string }) {
|
||||
@@ -22,10 +27,15 @@ function Kbd({ children }: { children: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeViewProps) {
|
||||
export default function WelcomeView({
|
||||
onNewSession,
|
||||
onQuickConnect,
|
||||
onRdpConnect,
|
||||
onVncConnect,
|
||||
}: WelcomeViewProps) {
|
||||
const { t } = useTranslation('sessions')
|
||||
const { addTab } = useTabStore()
|
||||
const requirePro = useEntitlementStore((s) => s.requirePro)
|
||||
const openUpgradeTab = useEntitlementStore((s) => s.openUpgradeTab)
|
||||
const sessions = useSessionStore((s) => s.sessions)
|
||||
const recentSessions = sessions.slice(-8).reverse()
|
||||
const modKey = useMemo(() => modKeyLabel(), [])
|
||||
@@ -70,11 +80,9 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
}, [defaultShell])
|
||||
|
||||
const handleSessionClick = (session: (typeof sessions)[number]) => {
|
||||
addTab({
|
||||
title: session.name || session.host,
|
||||
type: 'terminal',
|
||||
sessionId: session.id,
|
||||
})
|
||||
const protocol = session.protocol ?? 'ssh'
|
||||
if ((protocol === 'rdp' || protocol === 'vnc') && !requirePro(protocol)) return
|
||||
addTab(buildOpenTabPayload(session))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -91,11 +99,11 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-lg font-semibold tracking-tight text-content-primary">OpsXShell</h1>
|
||||
<p className="text-xs text-txt-muted">远程终端 · SFTP · 端口转发</p>
|
||||
<p className="text-xs text-txt-muted">{t('welcome.tagline')}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section aria-label="开始" className="mb-12">
|
||||
<section aria-label={t('welcome.sectionStart')} className="mb-12">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<button
|
||||
type="button"
|
||||
@@ -104,13 +112,13 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent-blue/15 text-accent-blue">
|
||||
<Zap className="h-4 w-4" strokeWidth={2} />
|
||||
<LogIn className="h-4 w-4" strokeWidth={2} />
|
||||
</span>
|
||||
<Kbd>{`${modKey}K`}</Kbd>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-content-primary">快速连接</div>
|
||||
<div className="mt-1 text-xs leading-relaxed text-txt-muted">临时 SSH,不写入会话库</div>
|
||||
<div className="text-sm font-medium text-content-primary">{t('welcome.quickConnect.title')}</div>
|
||||
<div className="mt-1 text-xs leading-relaxed text-txt-muted">{t('welcome.quickConnect.desc')}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -126,8 +134,8 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
<Kbd>{`${modKey}N`}</Kbd>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-content-primary">新建会话</div>
|
||||
<div className="mt-1 text-xs leading-relaxed text-txt-muted">保存主机,侧栏双击连接</div>
|
||||
<div className="text-sm font-medium text-content-primary">{t('welcome.newSession.title')}</div>
|
||||
<div className="mt-1 text-xs leading-relaxed text-txt-muted">{t('welcome.newSession.desc')}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -145,7 +153,7 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
<Terminal className="h-3.5 w-3.5 text-txt-faint" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-content-primary">本地 Shell</div>
|
||||
<div className="text-sm font-medium text-content-primary">{t('welcome.localShell.title')}</div>
|
||||
<div className="mt-1 truncate text-xs leading-relaxed text-txt-muted">
|
||||
{defaultShellLabel}
|
||||
</div>
|
||||
@@ -153,7 +161,7 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="选择 Shell"
|
||||
aria-label={t('welcome.localShell.selectShell')}
|
||||
aria-expanded={shellMenuOpen}
|
||||
onClick={() => setShellMenuOpen((v) => !v)}
|
||||
className="flex w-9 flex-shrink-0 items-center justify-center border-l border-dark-700/60 text-txt-faint transition-colors hover:bg-dark-700/40 hover:text-content-secondary"
|
||||
@@ -177,7 +185,7 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
<span className="w-10 font-mono text-[10px] uppercase text-txt-faint">{s.kind}</span>
|
||||
<span className="flex-1 text-sm text-content-primary">{s.name}</span>
|
||||
{s.isDefault && (
|
||||
<span className="text-[10px] text-txt-faint">默认</span>
|
||||
<span className="text-[10px] text-txt-faint">{t('welcome.localShell.default')}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
@@ -187,51 +195,51 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-label="Pro 即将推出" className="mb-10">
|
||||
<section aria-label={t('welcome.sectionDesktop')} className="mb-10">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<h2 className="text-[11px] font-semibold uppercase tracking-wider text-txt-faint">Pro · 即将推出</h2>
|
||||
<h2 className="text-[11px] font-semibold uppercase tracking-wider text-txt-faint">{t('welcome.sectionDesktop')}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!requirePro('rdp', { comingSoon: true })) return
|
||||
openUpgradeTab('rdp', true)
|
||||
if (!requirePro('rdp')) return
|
||||
onRdpConnect?.()
|
||||
}}
|
||||
className="flex items-center gap-3 rounded-xl border border-dark-700/50 bg-dark-800/30 px-4 py-3 text-left transition-colors hover:border-dark-600"
|
||||
className="flex items-center gap-3 rounded-xl border border-dark-700/50 bg-dark-800/30 px-4 py-3 text-left transition-colors hover:border-accent-blue/40"
|
||||
>
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-dark-700 text-txt-muted">
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent-blue/15 text-accent-blue">
|
||||
<Monitor className="h-4 w-4" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-content-secondary">RDP 远程桌面</div>
|
||||
<div className="text-[11px] text-txt-faint">Pro · 即将推出</div>
|
||||
<div className="text-sm font-medium text-content-primary">{t('welcome.rdp.title')}</div>
|
||||
<div className="text-[11px] text-txt-faint">{t('welcome.rdp.desc')}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!requirePro('vnc', { comingSoon: true })) return
|
||||
openUpgradeTab('vnc', true)
|
||||
if (!requirePro('vnc')) return
|
||||
onVncConnect?.()
|
||||
}}
|
||||
className="flex items-center gap-3 rounded-xl border border-dark-700/50 bg-dark-800/30 px-4 py-3 text-left transition-colors hover:border-dark-600"
|
||||
className="flex items-center gap-3 rounded-xl border border-dark-700/50 bg-dark-800/30 px-4 py-3 text-left transition-colors hover:border-accent-blue/40"
|
||||
>
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-dark-700 text-txt-muted">
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent-blue/15 text-accent-blue">
|
||||
<Cast className="h-4 w-4" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-content-secondary">VNC 远程桌面</div>
|
||||
<div className="text-[11px] text-txt-faint">Pro · 即将推出</div>
|
||||
<div className="text-sm font-medium text-content-primary">{t('welcome.vnc.title')}</div>
|
||||
<div className="text-[11px] text-txt-faint">{t('welcome.vnc.desc')}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-label="最近会话" className="min-h-0 flex-1">
|
||||
<section aria-label={t('welcome.sectionRecent')} className="min-h-0 flex-1">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Clock className="h-3.5 w-3.5 text-txt-faint" />
|
||||
<h2 className="text-[11px] font-semibold uppercase tracking-wider text-txt-faint">
|
||||
最近会话
|
||||
{t('welcome.sectionRecent')}
|
||||
</h2>
|
||||
{recentSessions.length > 0 && (
|
||||
<span className="text-[11px] text-txt-dim">{recentSessions.length}</span>
|
||||
@@ -242,8 +250,8 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
<div className="rounded-xl border border-dashed border-dark-700/70 px-4 py-10">
|
||||
<EmptyState
|
||||
icon={Server}
|
||||
title="暂无已保存会话"
|
||||
description="用快速连接试连,或新建会话以便复用"
|
||||
title={t('welcome.recentEmpty.title')}
|
||||
description={t('welcome.recentEmpty.desc')}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -255,8 +263,12 @@ export default function WelcomeView({ onNewSession, onQuickConnect }: WelcomeVie
|
||||
onClick={() => handleSessionClick(session)}
|
||||
className="group flex w-full items-center gap-3 px-3.5 py-2.5 text-left transition-colors hover:bg-dark-800/80"
|
||||
>
|
||||
<span className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-dark-800 text-txt-muted group-hover:text-accent-blue">
|
||||
<Server className="h-3.5 w-3.5" />
|
||||
<span className="flex h-8 w-8 flex-shrink-0 items-center justify-center">
|
||||
<HostDistroIcon
|
||||
distro={session.distro}
|
||||
protocol={session.protocol}
|
||||
size="md"
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm text-content-primary">{session.name}</span>
|
||||
|
||||
Reference in new issue
Block a user