import { useEffect, useRef, useState } from 'react' import { Monitor, RefreshCw, Send, X } from 'lucide-react' import RFBModule from '@novnc/novnc/lib/rfb' import { createVNCTicket, getWebVNCUrl } from '../services/api' type RFBConstructor = new ( target: HTMLElement, url: string, options?: { credentials?: Record; shared?: boolean; repeaterID?: string; wsProtocols?: string[] } ) => RFBInstance interface RFBInstance extends EventTarget { scaleViewport: boolean resizeSession: boolean focusOnClick: boolean viewOnly: boolean qualityLevel: number compressionLevel: number background: string disconnect(): void sendCtrlAltDel(): void } const RFB = resolveRFBConstructor(RFBModule) function resolveRFBConstructor(moduleValue: unknown): RFBConstructor { if (typeof moduleValue === 'function') { return moduleValue as RFBConstructor } const maybeDefault = (moduleValue as { default?: unknown })?.default if (typeof maybeDefault === 'function') { return maybeDefault as RFBConstructor } throw new Error('noVNC RFB constructor is unavailable') } interface WebVNCViewerProps { containerName: string onClose: () => void } export default function WebVNCViewer({ containerName, onClose }: WebVNCViewerProps) { const screenRef = useRef(null) const rfbRef = useRef(null) const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected' | 'error'>('connecting') const [errorMsg, setErrorMsg] = useState('') const cleanup = () => { if (rfbRef.current) { rfbRef.current.disconnect() rfbRef.current = null } } const ensureResizeObserver = () => { if ('ResizeObserver' in window) return class FallbackResizeObserver { private target: Element | null = null private timer = 0 private lastWidth = -1 private lastHeight = -1 constructor(private callback: ResizeObserverCallback) {} observe = (target: Element) => { this.target = target this.check() this.timer = window.setInterval(this.check, 250) window.addEventListener('resize', this.check) } unobserve = () => this.disconnect() disconnect = () => { if (this.timer) window.clearInterval(this.timer) this.timer = 0 window.removeEventListener('resize', this.check) this.target = null } private check = () => { if (!this.target) return const contentRect = this.target.getBoundingClientRect() if (contentRect.width === this.lastWidth && contentRect.height === this.lastHeight) return this.lastWidth = contentRect.width this.lastHeight = contentRect.height this.callback([{ target: this.target, contentRect } as ResizeObserverEntry], this as unknown as ResizeObserver) } } ;(window as unknown as { ResizeObserver: typeof ResizeObserver }).ResizeObserver = FallbackResizeObserver as unknown as typeof ResizeObserver } const connect = async () => { const target = screenRef.current if (!target) return cleanup() target.innerHTML = '' setStatus('connecting') setErrorMsg('') let ticket = '' try { const response = await createVNCTicket(containerName) ticket = response.data.data?.ticket || '' } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } } setStatus('error') setErrorMsg(error.response?.data?.message || 'WebVNC ticket 创建失败,请重新登录后再试') return } if (!ticket) { setStatus('error') setErrorMsg('WebVNC ticket 为空,请重新登录后再试') return } try { ensureResizeObserver() const rfb = new RFB(target, getWebVNCUrl(containerName), { wsProtocols: ['binary', `clicd-vnc-ticket.${ticket}`], }) rfb.scaleViewport = true rfb.resizeSession = false rfb.focusOnClick = true rfb.qualityLevel = 6 rfb.compressionLevel = 2 rfb.background = '#050505' rfb.addEventListener('connect', () => setStatus('connected')) rfb.addEventListener('disconnect', (event) => { const detail = (event as CustomEvent<{ clean?: boolean }>).detail setStatus((current) => current === 'error' ? current : 'disconnected') if (detail && detail.clean === false) { setErrorMsg('WebVNC 连接已断开,请确认虚拟机正在运行且 VNC 控制台可用') } }) rfb.addEventListener('securityfailure', () => { setStatus('error') setErrorMsg('VNC 安全协商失败') }) rfb.addEventListener('credentialsrequired', () => { setStatus('error') setErrorMsg('当前 VNC 控制台要求密码,暂不支持自动输入') }) rfbRef.current = rfb } catch (err) { console.error(err) setStatus('error') const message = err instanceof Error && err.message ? `:${err.message}` : '' setErrorMsg(`WebVNC 初始化失败${message}`) } } useEffect(() => { const timer = window.setTimeout(connect, 100) return () => { window.clearTimeout(timer) cleanup() } }, [containerName]) return (
WebVNC - {containerName} {status === 'connected' && 已连接} {status === 'connecting' && 连接中...} {status === 'disconnected' && 已断开} {status === 'error' && 连接失败}
{(status === 'connecting' || status === 'error' || (status === 'disconnected' && errorMsg)) && (
{status === 'connecting' ? '正在连接 KVM VNC 控制台...' : (errorMsg || 'WebVNC 已断开')}
)}
) }