新增 entitlement/license IPC 与 Waffo 一次性结账服务骨架,挂载 AI/转发/代理/串口/广播/批量导入导出门禁;批量执行改为结果矩阵并复用连接池,SFTP 工具栏图标化并去掉已连接条,同时完善跨平台本地终端与窗口配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
181 lines
5.0 KiB
TypeScript
181 lines
5.0 KiB
TypeScript
/**
|
|
* 快速连接 — 尽量像 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) return
|
|
setTarget(initialTarget)
|
|
setPassword('')
|
|
setError(null)
|
|
setIsConnecting(false)
|
|
}, [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="快速连接"
|
|
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>
|
|
)
|
|
}
|