vdi/web-fe/src/pages/vncClient/mod/index.tsx

401 lines
12 KiB
TypeScript
Raw Normal View History

2025-08-29 09:51:17 +00:00
/* eslint-disable @typescript-eslint/no-use-before-define */
import type { MenuProps } from 'antd';
import { Button, Dropdown } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
// import { RFB } from '@novnc/novnc/core/rfb';
import RFB from '@/public/novnc/core/rfb';
import './index.less';
interface VncRemoteDesktopProps {
vncUrl: string;
password?: string;
onConnected?: () => void;
onDisconnected?: () => void;
onError?: (error: string) => void;
loadingText?: string;
className?: string;
isFullscreen?: boolean;
viewOnly?: boolean;
autoScale?: boolean;
maxRetries?: number;
retryInterval?: number;
}
/**
* noVNC
* 使@novnc/novnc
*/
const VncRemoteDesktop: React.FC<VncRemoteDesktopProps> = ({
vncUrl,
password,
onConnected,
onDisconnected,
onError,
loadingText = '正在连接远程桌面...',
className = '',
viewOnly = false,
autoScale = true,
maxRetries = 3,
retryInterval = 2000,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const rfbRef = useRef<RFB | null>(null);
const retryCountRef = useRef(0);
const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [connectionStatus, setConnectionStatus] = useState<
'disconnected' | 'connecting' | 'connected'
>('disconnected');
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [visible, setVisible] = useState(false);
// 监听浏览器窗口关闭事件
useEffect(() => {
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault();
disconnect();
return '';
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, []);
// 清除重试定时器
const clearRetryTimeout = () => {
if (retryTimeoutRef.current) {
clearTimeout(retryTimeoutRef.current);
retryTimeoutRef.current = null;
}
};
// 连接到VNC服务器
const connect = (resetRetry = false) => {
if (!vncUrl || !canvasRef.current) {
return;
}
// 清除之前的重试定时器
clearRetryTimeout();
// 重置重试计数(如果需要)
if (resetRetry) {
retryCountRef.current = 0;
}
// 断开已有连接
if (rfbRef.current) {
disconnect();
}
setConnectionStatus('connecting');
setErrorMessage(null);
try {
// 验证URL格式
if (!vncUrl.startsWith('ws://') && !vncUrl.startsWith('wss://')) {
throw new Error('无效的VNC URL格式请使用ws://或wss://开头');
}
console.log('WebSocket URL=========', vncUrl);
console.log('尝试连接到VNC服务器:', vncUrl);
// 创建RFB实例
const rfb = new RFB(canvasRef.current, vncUrl, {
credentials: password ? { password } : undefined,
shared: true,
wsProtocols: ['binary'],
// focusOnClick: !viewOnly,
dragViewport: true,
scaleViewport: true,
resizeSession: true,
// viewOnly: viewOnly,
// background: '#000000',
});
// 保存RFB实例引用用于后续操作和事件处理
rfbRef.current = rfb;
console.log('rfbRef.current=====保存RFB实例引用', rfbRef.current);
// 监听连接事件
rfb.addEventListener('connect', () => {
console.log('VNC连接成功');
retryCountRef.current = 0; // 重置重试计数
setConnectionStatus('connected');
onConnected?.();
});
// 监听断开连接事件
rfb.addEventListener('disconnect', () => {
console.log('VNC连接断开');
setConnectionStatus('disconnected');
onDisconnected?.();
console.log('rfbRef.current=====监听断开连接事件', rfbRef.current);
});
// 监听安全失败事件
rfb.addEventListener('securityfailure', (e: any) => {
console.error('VNC安全连接失败:', e.detail);
setErrorMessage('安全连接失败: ' + e.detail);
setConnectionStatus('disconnected');
onError?.('安全连接失败: ' + e.detail);
console.log('rfbRef.current=====监听安全失败事件', rfbRef.current);
rfbRef.current = null;
});
// 监听凭证请求事件
rfb.addEventListener('credentialsrequired', () => {
console.log('需要凭证');
if (password && rfbRef.current) {
rfbRef.current.sendCredentials({ password });
}
});
// 监听错误事件
rfb.addEventListener('error', (e: any) => {
const errorDetail = e.detail || {};
const errorMsg = errorDetail.message || errorDetail || '未知错误';
console.error('VNC错误:', errorMsg);
// 提供更具体的错误信息
let userFriendlyError = '';
if (errorMsg.toString().includes('WebSocket')) {
userFriendlyError = `WebSocket连接失败: ${errorMsg}\n可能的原因: 服务器不可达、网络问题或防火墙阻止`;
} else if (errorMsg.toString().includes('401')) {
userFriendlyError = '认证失败: 用户名或密码错误';
} else if (errorMsg.toString().includes('403')) {
userFriendlyError = '权限不足: 您没有访问该远程桌面的权限';
} else if (errorMsg.toString().includes('timeout')) {
userFriendlyError = '连接超时: 服务器响应超时,请检查网络连接';
} else {
userFriendlyError = `连接错误: ${errorMsg}`;
}
setErrorMessage(userFriendlyError);
setConnectionStatus('disconnected');
onError?.(userFriendlyError);
// 自动重试连接(如果未达到最大重试次数)
if (retryCountRef.current < maxRetries) {
retryCountRef.current++;
console.log(`尝试重新连接 (${retryCountRef.current}/${maxRetries})`);
retryTimeoutRef.current = setTimeout(() => {
connect();
}, retryInterval);
}
});
console.log('RFB连接配置完成');
} catch (error) {
console.error('创建VNC连接失败:', error);
const errorMsg = error instanceof Error ? error.message : '创建连接失败';
setErrorMessage(errorMsg);
setConnectionStatus('disconnected');
onError?.(errorMsg);
}
};
// 断开连接
const disconnect = () => {
// 清除重试定时器
clearRetryTimeout();
console.log('rfbRef.current=====断开连接', rfbRef);
console.log('rfbRef.current=====断开连接', rfbRef.current);
if (rfbRef.current) {
try {
// 确保完全断开连接并释放所有资源
rfbRef.current.disconnect();
} catch (error) {
console.error('Error during disconnect/cleanup:', error);
}
}
// 重置重试计数
retryCountRef.current = 0;
};
// 连接
const reconnect = () => {
setVisible(true);
connect(true); // 重置重试计数后连接
};
// 当vncUrl或password变化时重新连接
useEffect(() => {
if (vncUrl && visible) {
connect(true); // 重置重试计数后连接
}
// 组件卸载时断开连接并清理资源
return () => {
disconnect();
clearRetryTimeout();
};
}, [visible, vncUrl, password, maxRetries, retryInterval]);
// 当autoScale或viewOnly属性变化时更新RFB实例
useEffect(() => {
if (rfbRef.current) {
rfbRef.current.viewOnly = viewOnly;
rfbRef.current.focusOnClick = !viewOnly;
rfbRef.current.dragViewport = !viewOnly;
rfbRef.current.scaleViewport = autoScale;
rfbRef.current.resizeSession = autoScale;
}
}, [viewOnly, autoScale]);
const menuItems: MenuProps = {
items: [
{
key: '1',
label: <div></div>,
// label: (
// <Button
// onClick={() => {}}
// disabled={connectionStatus === 'connecting'}
// title="挂载优化工具"
// >
// 挂载优化工具
// </Button>
// ),
},
{
key: '2', // 注意 key 应该唯一
label: <div></div>,
// label: (
// <Button
// onClick={() => {}}
// disabled={connectionStatus === 'connecting'}
// title="挂载应用软件盘"
// >
// 挂载应用软件盘
// </Button>
// ),
},
{
key: '3',
label: <div></div>,
// label: (
// <Button
// onClick={() => {}}
// disabled={connectionStatus === 'connecting'}
// title="挂载应用软件盘"
// >
// 挂载应用软件盘
// </Button>
// ),
},
],
};
return (
<div className={`vnc-remote-desktop ${className} ${connectionStatus}`}>
{/* 控制栏 */}
<div className="vnc-controls">
<span className={`vnc-status-indicator ${connectionStatus}`}>
{connectionStatus === 'connecting' && '连接中...'}
{connectionStatus === 'connected' && '已连接'}
{connectionStatus === 'disconnected' && '已断开'}
</span>
<div className="vnc-actions">
{viewOnly && <span className="view-only-indicator"></span>}
{!(connectionStatus === 'connected') ? (
<Button
onClick={reconnect}
disabled={connectionStatus === 'connecting'}
title="重新连接"
>
</Button>
) : (
<>
<Button
onClick={() => {}}
disabled={!(connectionStatus === 'connected')}
title="关机"
>
</Button>
<Button
onClick={() => {}}
disabled={!(connectionStatus === 'connected')}
title="重启"
>
</Button>
<Dropdown
menu={menuItems}
disabled={!(connectionStatus === 'connected')}
>
<Button
onClick={() => {}}
disabled={!(connectionStatus === 'connected')}
title="安装模板工具"
>
</Button>
</Dropdown>
<Button
onClick={disconnect}
disabled={!(connectionStatus === 'connected')}
title="断开连接"
type="default"
>
</Button>
</>
)}
</div>
</div>
{/* 远程桌面内容 */}
<div className="vnc-content">
{/* 加载状态 */}
{connectionStatus === 'connecting' && (
<div className="vnc-loading-overlay">
<div className="loading-spinner"></div>
<div>{loadingText}</div>
</div>
)}
{/* 错误状态 */}
{errorMessage && connectionStatus === 'disconnected' && (
<div className="vnc-error-overlay">
<div className="error-icon"></div>
<div className="error-message">{errorMessage}</div>
<Button className="reconnect-button" onClick={reconnect}>
</Button>
</div>
)}
{/* VNC画布,使用canvas远程桌面控制页面无法展示不要使用 */}
<div
ref={canvasRef}
className="vnc-canvas"
tabIndex={viewOnly ? -1 : 0}
style={{
display: connectionStatus === 'connected' ? 'block' : 'none',
}}
/>
{/* 未连接时的占位符 */}
{connectionStatus === 'disconnected' && !errorMessage && (
<div className="vnc-placeholder">
<div className="placeholder-icon">🖥</div>
<div>...</div>
</div>
)}
</div>
</div>
);
};
export default VncRemoteDesktop;