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,198 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Cable, RefreshCw } from 'lucide-react'
|
||||
import Modal from '../common/Modal'
|
||||
import { listSerialPorts } from '../../platform/serial'
|
||||
import type {
|
||||
SerialConfig,
|
||||
SerialFlowControl,
|
||||
SerialParity,
|
||||
SerialPortSummary,
|
||||
} from '../../../shared/types/serial.types'
|
||||
|
||||
interface SerialConnectDialogProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onConnect: (config: SerialConfig) => void
|
||||
}
|
||||
|
||||
const BAUD_RATES = [9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600]
|
||||
|
||||
export default function SerialConnectDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConnect,
|
||||
}: SerialConnectDialogProps) {
|
||||
const [ports, setPorts] = useState<SerialPortSummary[]>([])
|
||||
const [path, setPath] = useState('/dev/tty.debug-console')
|
||||
const [baudRate, setBaudRate] = useState(115200)
|
||||
const [dataBits, setDataBits] = useState<5 | 6 | 7 | 8>(8)
|
||||
const [stopBits, setStopBits] = useState<1 | 2>(1)
|
||||
const [parity, setParity] = useState<SerialParity>('none')
|
||||
const [flowControl, setFlowControl] = useState<SerialFlowControl>('none')
|
||||
const [loadingPorts, setLoadingPorts] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const refreshPorts = useCallback(async () => {
|
||||
setLoadingPorts(true)
|
||||
setError(null)
|
||||
try {
|
||||
const detected = await listSerialPorts()
|
||||
setPorts(detected)
|
||||
setPath((currentPath) =>
|
||||
detected.length > 0 && !detected.some((port) => port.path === currentPath)
|
||||
? detected[0].path
|
||||
: currentPath
|
||||
)
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : String(reason))
|
||||
} finally {
|
||||
setLoadingPorts(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) void refreshPorts()
|
||||
}, [isOpen, refreshPorts])
|
||||
|
||||
const handleConnect = () => {
|
||||
if (!path.trim()) {
|
||||
setError('请输入串口设备路径')
|
||||
return
|
||||
}
|
||||
onConnect({
|
||||
path: path.trim(),
|
||||
baudRate,
|
||||
dataBits,
|
||||
stopBits,
|
||||
parity,
|
||||
flowControl,
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onConfirm={handleConnect}
|
||||
title="连接串口"
|
||||
description="原生串口终端 · 适用于路由器、交换机、开发板与调试控制台"
|
||||
icon={<Cable className="w-5 h-5" />}
|
||||
confirmText="打开串口"
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<section className="rounded-xl border border-dark-700/60 bg-dark-900/30 p-3.5 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="serial-path" className="ui-label mb-0">设备</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void refreshPorts()}
|
||||
className="inline-flex items-center gap-1 text-[11px] text-txt-muted hover:text-content-primary"
|
||||
>
|
||||
<RefreshCw className={`w-3 h-3 ${loadingPorts ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
id="serial-path"
|
||||
value={path}
|
||||
onChange={(event) => setPath(event.target.value)}
|
||||
placeholder="/dev/tty.debug-console"
|
||||
className="ui-input font-mono"
|
||||
spellCheck={false}
|
||||
/>
|
||||
{ports.length > 0 ? (
|
||||
<div className="grid gap-1.5 max-h-32 overflow-y-auto">
|
||||
{ports.map((port) => (
|
||||
<button
|
||||
key={port.path}
|
||||
type="button"
|
||||
onClick={() => setPath(port.path)}
|
||||
className={`flex items-center justify-between gap-3 px-3 py-2 rounded-lg border text-left transition-colors ${
|
||||
path === port.path
|
||||
? 'border-accent-blue/60 bg-accent-blue/10 text-content-primary'
|
||||
: 'border-dark-700/50 hover:bg-dark-700/40 text-content-secondary'
|
||||
}`}
|
||||
>
|
||||
<span className="font-mono text-xs truncate">{port.path}</span>
|
||||
<span className="text-[10px] text-txt-faint flex-shrink-0">{port.portType}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[11px] text-txt-faint">
|
||||
未枚举到设备时可直接输入路径,例如 /dev/tty.debug-console 或
|
||||
/dev/tty.Bluetooth-Incoming-Port。
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-2 sm:grid-cols-3 gap-3 rounded-xl border border-dark-700/60 bg-dark-900/30 p-3.5">
|
||||
<div>
|
||||
<label htmlFor="serial-baud" className="ui-label">波特率</label>
|
||||
<select
|
||||
id="serial-baud"
|
||||
value={baudRate}
|
||||
onChange={(event) => setBaudRate(Number(event.target.value))}
|
||||
className="ui-input-sm"
|
||||
>
|
||||
{BAUD_RATES.map((rate) => <option key={rate} value={rate}>{rate}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="serial-data-bits" className="ui-label">数据位</label>
|
||||
<select
|
||||
id="serial-data-bits"
|
||||
value={dataBits}
|
||||
onChange={(event) => setDataBits(Number(event.target.value) as 5 | 6 | 7 | 8)}
|
||||
className="ui-input-sm"
|
||||
>
|
||||
{[5, 6, 7, 8].map((value) => <option key={value} value={value}>{value}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="serial-stop-bits" className="ui-label">停止位</label>
|
||||
<select
|
||||
id="serial-stop-bits"
|
||||
value={stopBits}
|
||||
onChange={(event) => setStopBits(Number(event.target.value) as 1 | 2)}
|
||||
className="ui-input-sm"
|
||||
>
|
||||
<option value={1}>1</option>
|
||||
<option value={2}>2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="serial-parity" className="ui-label">校验</label>
|
||||
<select
|
||||
id="serial-parity"
|
||||
value={parity}
|
||||
onChange={(event) => setParity(event.target.value as SerialParity)}
|
||||
className="ui-input-sm"
|
||||
>
|
||||
<option value="none">无</option>
|
||||
<option value="odd">奇校验</option>
|
||||
<option value="even">偶校验</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<label htmlFor="serial-flow-control" className="ui-label">流控</label>
|
||||
<select
|
||||
id="serial-flow-control"
|
||||
value={flowControl}
|
||||
onChange={(event) => setFlowControl(event.target.value as SerialFlowControl)}
|
||||
className="ui-input-sm"
|
||||
>
|
||||
<option value="none">无</option>
|
||||
<option value="software">软件 XON/XOFF</option>
|
||||
<option value="hardware">硬件 RTS/CTS</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user