feat: release OpsXShell 2.1.0 native Tauri edition

This commit is contained in:
Logan.Li committed 2026-07-30 23:32:51 +08:00
1 parent e9a0d21758
commit a3dba0c6ed
311 files changed
+31748 -14897

No files matched your search

@@ -0,0 +1,180 @@
/**
* 快速连接 — 尽量像 ssh 命令一样简单
* 支持:host | user@host | user@host:port | host:port
*/
import { useEffect, useState } from 'react'
import { Zap, ArrowRight, AlertCircle } from 'lucide-react'
import { useTabStore } from '../../stores/tabStore'
import Modal from '../common/Modal'
import Button from '../common/Button'
interface QuickConnectDialogProps {
isOpen: boolean
onClose: () => void
initialTarget?: string
}
/** 解析 user@host:port / host:port / host */
// eslint-disable-next-line react-refresh/only-export-components
export function parseSshTarget(raw: string): {
host: string
port: number
username: string
} | null {
const s = raw.trim()
if (!s) return null
// strip optional ssh://
const cleaned = s.replace(/^ssh:\/\//i, '')
let username = 'root'
let hostPort = cleaned
const at = cleaned.lastIndexOf('@')
if (at > 0) {
username = cleaned.slice(0, at).trim() || 'root'
hostPort = cleaned.slice(at + 1).trim()
}
// IPv6 in brackets [2001:db8::1]:22
let host = hostPort
let port = 22
const bracket = /^\[([^\]]+)\](?::(\d+))?$/.exec(hostPort)
if (bracket) {
host = bracket[1]
if (bracket[2]) port = parseInt(bracket[2], 10) || 22
} else {
// last :port — avoid splitting bare IPv6 without brackets
const colon = hostPort.lastIndexOf(':')
if (colon > 0 && hostPort.indexOf(':') === colon) {
const maybePort = hostPort.slice(colon + 1)
if (/^\d+$/.test(maybePort)) {
host = hostPort.slice(0, colon)
port = parseInt(maybePort, 10) || 22
}
}
}
host = host.trim()
if (!host || !username) return null
return { host, port, username }
}
export default function QuickConnectDialog({ isOpen, onClose, initialTarget = '' }: QuickConnectDialogProps) {
const { addTab } = useTabStore()
const [target, setTarget] = useState('')
const [password, setPassword] = useState('')
const [isConnecting, setIsConnecting] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (isOpen && initialTarget) {
setTarget(initialTarget)
setError(null)
}
}, [initialTarget, isOpen])
const handleSubmit = async () => {
const parsed = parseSshTarget(target)
if (!parsed) {
setError('请输入主机,例如 root@192.168.1.10 或 opsbase.cn:5555')
return
}
setIsConnecting(true)
setError(null)
try {
addTab({
title: `${parsed.username}@${parsed.host}`,
type: 'terminal',
connectionConfig: {
host: parsed.host,
port: parsed.port,
username: parsed.username,
password: password || undefined,
},
})
onClose()
setTarget('')
setPassword('')
} catch (err: unknown) {
setError((err instanceof Error ? err.message : String(err)) || '连接失败')
} finally {
setIsConnecting(false)
}
}
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="快速连接"
description="粘贴 user@host:port 即可,不必拆成多个框"
icon={<Zap className="h-5 w-5" />}
size="md"
showFooter={false}
>
<form
onSubmit={(e) => {
e.preventDefault()
void handleSubmit()
}}
className="space-y-4"
>
<div>
<label className="ui-label">目标</label>
<input
type="text"
required
autoFocus
value={target}
onChange={(e) => setTarget(e.target.value)}
placeholder="root@192.168.1.10:22"
className="ui-input font-mono"
autoComplete="off"
spellCheck={false}
/>
<p className="mt-1.5 text-[11px] leading-relaxed text-txt-faint">支持 host、user@host、user@host:port</p>
</div>
<div>
<label className="ui-label">
密码 <span className="font-normal text-txt-faint">可选 · 用密钥可留空</span>
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="ui-input"
autoComplete="current-password"
/>
</div>
{error && (
<div className="flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2.5 text-sm text-red-300">
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
<span>{error}</span>
</div>
)}
<div className="border-dark-700/50 flex justify-end gap-2.5 border-t pt-2">
<Button type="button" variant="ghost" size="sm" onClick={onClose} disabled={isConnecting}>
取消
</Button>
<Button
type="submit"
variant="primary"
size="sm"
loading={isConnecting}
icon={<ArrowRight className="h-3.5 w-3.5" />}
>
连接
</Button>
</div>
</form>
</Modal>
)
}