feat: Add explicit Apply button to Sandbox Node Binding UI

This commit is contained in:
Haitao Pan 2026-02-06 12:19:28 +08:00
parent 9bb99e5509
commit 97d05ce612

View File

@ -44,65 +44,102 @@ export default function SandboxNodeBindingPanel() {
})
const [message, setMessage] = useState<string | null>(null)
const current = getSandboxNodeBinding()
const selectedAddress = current?.address ?? ''
const currentBinding = useMemo(() => getSandboxNodeBinding(), [])
const [draftAddress, setDraftAddress] = useState<string>(currentBinding?.address ?? '')
const selectedNode = useMemo(
() => nodes?.find((node) => node.address === selectedAddress) ?? null,
[nodes, selectedAddress],
)
const isChanged = useMemo(() => {
const current = getSandboxNodeBinding()
return (current?.address ?? '') !== draftAddress
}, [draftAddress])
const handleApply = () => {
const address = draftAddress.trim()
if (!address) {
clearSandboxNodeBinding()
setMessage('已成功清空绑定节点')
} else {
const node = nodes?.find((item) => item.address === address)
if (!node) {
setMessage('错误:选择的节点不存在')
return
}
setSandboxNodeBinding({
address: node.address,
name: node.name,
updatedBy: 'root',
})
setMessage(`应用成功:已绑定至 ${node.name || node.address}`)
}
}
const currentActive = getSandboxNodeBinding()
return (
<Card>
<div className="space-y-3">
<h2 className="text-lg font-semibold text-gray-900">Root Sandbox Node </h2>
<p className="text-sm text-gray-600"> 1 Sandbox@svc.plus 使 VLESS </p>
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold text-gray-900">Root Sandbox Node </h2>
<p className="text-sm text-gray-600">Sandbox@svc.plus 使</p>
</div>
<label className="flex flex-col gap-2 text-sm text-gray-700">
<select
value={selectedAddress}
disabled={isLoading || !nodes || nodes.length === 0}
onChange={(event) => {
const address = event.target.value.trim()
if (!address) {
clearSandboxNodeBinding()
setMessage('已清空绑定节点')
return
}
const node = nodes?.find((item) => item.address === address)
if (!node) {
setMessage('节点不存在,无法绑定')
return
}
setSandboxNodeBinding({
address: node.address,
name: node.name,
updatedBy: 'root',
})
setMessage(`已绑定:${node.name || node.address}`)
}}
className="rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-800 focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-200"
<div className="flex items-end gap-3">
<label className="flex flex-1 flex-col gap-2 text-sm font-medium text-gray-700">
<select
value={draftAddress}
disabled={isLoading || !nodes}
onChange={(e) => {
setDraftAddress(e.target.value)
setMessage(null)
}}
className="rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-800 focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-200"
>
<option value=""></option>
{(nodes ?? []).map((node) => (
<option key={node.address} value={node.address}>
{node.name} ({node.address})
</option>
))}
</select>
</label>
<button
onClick={handleApply}
disabled={!isChanged}
className={`rounded-lg px-4 py-2 text-sm font-medium transition-colors ${isChanged
? 'bg-purple-600 text-white hover:bg-purple-700 shadow-sm'
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
}`}
>
<option value=""></option>
{(nodes ?? []).map((node) => (
<option key={node.address} value={node.address}>
{node.name} ({node.address})
</option>
))}
</select>
</label>
</button>
</div>
{selectedNode ? (
<p className="text-xs text-gray-600">
<span className="font-medium text-gray-800">{selectedNode.name || selectedNode.address}</span>
<div className="space-y-1 rounded-md bg-gray-50 p-3">
{currentActive ? (
<div className="flex items-center gap-2 text-xs text-gray-700">
<div className="h-2 w-2 rounded-full bg-green-500" />
<span className="font-bold">{currentActive.name || currentActive.address}</span>
</div>
) : (
<div className="flex items-center gap-2 text-xs text-gray-500">
<div className="h-2 w-2 rounded-full bg-gray-300" />
</div>
)}
{currentActive?.updatedAt && (
<p className="pl-4 text-[10px] text-gray-400">
{new Date(currentActive.updatedAt).toLocaleString()}
</p>
)}
</div>
{error && <p className="text-xs text-red-600"> {error.message}</p>}
{message && (
<p className={`text-xs font-medium ${message.startsWith('错误') ? 'text-red-600' : 'text-green-600'}`}>
{message}
</p>
) : null}
{current?.updatedAt ? (
<p className="text-xs text-gray-500">{new Date(current.updatedAt).toLocaleString()}</p>
) : null}
{error ? <p className="text-xs text-red-600">{error.message}</p> : null}
{message ? <p className="text-xs text-green-700">{message}</p> : null}
)}
</div>
</Card>
)