feat: release OpsXShell 2.1.0 native Tauri edition
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user