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

401 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* 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;