401 lines
12 KiB
TypeScript
401 lines
12 KiB
TypeScript
/* 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;
|