feat: 完善堡垒机与 Agent 智能运维能力
- 支持 Windows Agent 构建、安装脚本和统一上报协议 - 增强 RDP 剪贴板、虚拟盘和文件传输能力 - 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口 - 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
1 parent
a9513c2056
commit
e0d652d211
169 files changed
+12192
-9116
No files matched your search
@@ -1,17 +1,23 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, type KeyboardEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getWsBase } from '@/utils/request';
|
||||
import { useUserStore } from '@/store';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover';
|
||||
import { RdpToolsPanel } from './RdpToolsPanel';
|
||||
import { Loader2, Maximize2, Minimize2, Wrench, X } from 'lucide-react';
|
||||
} from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getWsBase } from "@/utils/request";
|
||||
import { useSessionSecurityStore, useUserStore } from "@/store";
|
||||
import { copyToClipboard, readFromClipboard } from "@/utils/clipboard";
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
} from "@/components/ui/popover";
|
||||
import { RdpToolsPanel } from "./RdpToolsPanel";
|
||||
import { SessionWatermark } from "@/components/common/SessionWatermark";
|
||||
import { Loader2, Maximize2, Minimize2, Wrench, X } from "lucide-react";
|
||||
|
||||
interface RdpDialogProps {
|
||||
open: boolean;
|
||||
@@ -30,6 +36,9 @@ interface RdpFrame {
|
||||
image?: string;
|
||||
message?: string;
|
||||
text?: string;
|
||||
name?: string;
|
||||
url?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
interface RdpStats {
|
||||
@@ -38,9 +47,15 @@ interface RdpStats {
|
||||
failed: number;
|
||||
}
|
||||
|
||||
const RDP_WIDTH = 1024;
|
||||
const RDP_HEIGHT = 800;
|
||||
const RDP_DEFAULT_WIDTH = 900;
|
||||
const RDP_DEFAULT_HEIGHT = 620;
|
||||
const RDP_MIN_WIDTH = 720;
|
||||
const RDP_MIN_HEIGHT = 520;
|
||||
const RDP_MAX_WIDTH = 1920;
|
||||
const RDP_MAX_HEIGHT = 1200;
|
||||
const RDP_BINARY_HEADER = 20;
|
||||
const RDP_HEADER_HEIGHT = 41;
|
||||
const RDP_DIALOG_VERTICAL_GAP = 96;
|
||||
|
||||
interface RdpRawFrame {
|
||||
x: number;
|
||||
@@ -50,27 +65,87 @@ interface RdpRawFrame {
|
||||
rgba: Uint8ClampedArray;
|
||||
}
|
||||
|
||||
export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }: RdpDialogProps) {
|
||||
function clampRdpSize(value: number, min: number, max: number) {
|
||||
const clamped = Math.max(min, Math.min(max, value));
|
||||
return clamped % 2 === 0 ? clamped : clamped - 1;
|
||||
}
|
||||
|
||||
function calcRdpViewport(fullscreen: boolean) {
|
||||
if (!fullscreen) {
|
||||
return {
|
||||
// 13 寸屏默认保留外层操作空间,最大化时再提升远端分辨率。 // Logan.Li
|
||||
width: Math.min(
|
||||
RDP_DEFAULT_WIDTH,
|
||||
clampRdpSize(
|
||||
Math.floor(window.innerWidth * 0.78),
|
||||
RDP_MIN_WIDTH,
|
||||
RDP_DEFAULT_WIDTH,
|
||||
),
|
||||
),
|
||||
height: Math.min(
|
||||
RDP_DEFAULT_HEIGHT,
|
||||
clampRdpSize(
|
||||
Math.floor(window.innerHeight - 180),
|
||||
RDP_MIN_HEIGHT,
|
||||
RDP_DEFAULT_HEIGHT,
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
const maxWidth = fullscreen
|
||||
? window.innerWidth
|
||||
: Math.min(window.innerWidth * 0.94, RDP_MAX_WIDTH);
|
||||
const maxHeight = fullscreen
|
||||
? window.innerHeight - RDP_HEADER_HEIGHT
|
||||
: window.innerHeight - RDP_DIALOG_VERTICAL_GAP;
|
||||
return {
|
||||
width: clampRdpSize(Math.floor(maxWidth), RDP_MIN_WIDTH, RDP_MAX_WIDTH),
|
||||
height: clampRdpSize(Math.floor(maxHeight), RDP_MIN_HEIGHT, RDP_MAX_HEIGHT),
|
||||
};
|
||||
}
|
||||
|
||||
export function RdpDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
serverId,
|
||||
serverName,
|
||||
serverIp,
|
||||
}: RdpDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const paintQueueRef = useRef<Promise<void>>(Promise.resolve());
|
||||
const statsRef = useRef<RdpStats>({ received: 0, drawn: 0, failed: 0 });
|
||||
const pendingMouseMoveRef = useRef<Record<string, unknown> | null>(null);
|
||||
const mouseMoveTimerRef = useRef<number | null>(null);
|
||||
const keyModifierRef = useRef({
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
meta: false,
|
||||
});
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const [status, setStatus] = useState('等待连接');
|
||||
const [status, setStatus] = useState(t("server.rdpDialog.waiting"));
|
||||
const [hasFrame, setHasFrame] = useState(false);
|
||||
const [canvasSize, setCanvasSize] = useState({ width: RDP_WIDTH, height: RDP_HEIGHT });
|
||||
const [displaySize, setDisplaySize] = useState({ width: RDP_WIDTH, height: RDP_HEIGHT });
|
||||
const [canvasSize, setCanvasSize] = useState(() => calcRdpViewport(false));
|
||||
const [displaySize, setDisplaySize] = useState(() => calcRdpViewport(false));
|
||||
const [toolsOpen, setToolsOpen] = useState(false);
|
||||
const [remoteClipboard, setRemoteClipboard] = useState('');
|
||||
const [remoteClipboard, setRemoteClipboard] = useState("");
|
||||
const clipboardEnabled = useSessionSecurityStore(
|
||||
(state) => state.clipboardEnabled,
|
||||
);
|
||||
const fileDownloadEnabled = useSessionSecurityStore(
|
||||
(state) => state.fileDownloadEnabled,
|
||||
);
|
||||
|
||||
const sendEvent = (event: Record<string, unknown>) => {
|
||||
const ws = wsRef.current;
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(event));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const flushMouseMove = () => {
|
||||
@@ -95,17 +170,90 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
}, 16);
|
||||
};
|
||||
|
||||
const patchKeyModifier = (
|
||||
e: KeyboardEvent<HTMLCanvasElement>,
|
||||
down: boolean,
|
||||
) => {
|
||||
const next = keyModifierRef.current;
|
||||
if (e.code === "ControlLeft" || e.code === "ControlRight") next.ctrl = down;
|
||||
if (e.code === "AltLeft" || e.code === "AltRight") next.alt = down;
|
||||
if (e.code === "ShiftLeft" || e.code === "ShiftRight") next.shift = down;
|
||||
if (e.code === "MetaLeft" || e.code === "MetaRight") next.meta = down;
|
||||
};
|
||||
|
||||
const currentModifiers = (e: KeyboardEvent<HTMLCanvasElement>) => {
|
||||
const state = keyModifierRef.current;
|
||||
// keyup 事件经常丢失 ctrlKey/metaKey,使用本地状态保证组合键释放也能发到远端。 // Logan.Li
|
||||
return {
|
||||
ctrl: state.ctrl || e.ctrlKey,
|
||||
alt: state.alt || e.altKey,
|
||||
shift: state.shift || e.shiftKey,
|
||||
meta: state.meta || e.metaKey,
|
||||
};
|
||||
};
|
||||
|
||||
const handleKeyDown = async (e: KeyboardEvent<HTMLCanvasElement>) => {
|
||||
e.preventDefault();
|
||||
patchKeyModifier(e, true);
|
||||
const modifiers = currentModifiers(e);
|
||||
const pasteCombo = (e.metaKey || e.ctrlKey) && e.code === "KeyV";
|
||||
if (pasteCombo) {
|
||||
if (!clipboardEnabled) {
|
||||
setStatus(t("server.sessionPolicy.clipboardDisabled"));
|
||||
return;
|
||||
}
|
||||
const text = await readFromClipboard();
|
||||
if (text) sendEvent({ type: "clipboard", text });
|
||||
return;
|
||||
}
|
||||
sendEvent({
|
||||
type: "key",
|
||||
code: e.code,
|
||||
key: e.key,
|
||||
down: true,
|
||||
...modifiers,
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent<HTMLCanvasElement>) => {
|
||||
e.preventDefault();
|
||||
if ((e.metaKey || e.ctrlKey) && e.code === "KeyV") return;
|
||||
const modifiers = currentModifiers(e);
|
||||
patchKeyModifier(e, false);
|
||||
sendEvent({
|
||||
type: "key",
|
||||
code: e.code,
|
||||
key: e.key,
|
||||
down: false,
|
||||
...modifiers,
|
||||
});
|
||||
};
|
||||
|
||||
const requestRemoteCopy = () => {
|
||||
// 工具按钮使用独立事件,避免和用户在远端窗口手按 Ctrl+C 的键盘状态互相污染。 // Logan.Li
|
||||
if (!clipboardEnabled) {
|
||||
setStatus(t("server.sessionPolicy.clipboardDisabled"));
|
||||
return;
|
||||
}
|
||||
flushMouseMove();
|
||||
sendEvent({ type: "remote_copy" });
|
||||
setStatus(t("server.rdpDialog.remoteCopyRequested"));
|
||||
};
|
||||
|
||||
const patchStats = (patch: Partial<RdpStats>) => {
|
||||
statsRef.current = { ...statsRef.current, ...patch };
|
||||
};
|
||||
|
||||
const paintBitmap = async (ctx: CanvasRenderingContext2D, frame: RdpFrame) => {
|
||||
const paintBitmap = async (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
frame: RdpFrame,
|
||||
) => {
|
||||
const dx = frame.x || 0;
|
||||
const dy = frame.y || 0;
|
||||
const dw = frame.width || 0;
|
||||
const dh = frame.height || 0;
|
||||
try {
|
||||
if ('createImageBitmap' in window && frame.image) {
|
||||
if ("createImageBitmap" in window && frame.image) {
|
||||
const blob = await fetch(frame.image).then((resp) => resp.blob());
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
ctx.drawImage(bitmap, dx, dy, dw || bitmap.width, dh || bitmap.height);
|
||||
@@ -122,12 +270,15 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
ctx.drawImage(img, dx, dy, dw || img.width, dh || img.height);
|
||||
resolve();
|
||||
};
|
||||
img.onerror = () => reject(new Error('RDP 位图解码失败'));
|
||||
img.src = frame.image || '';
|
||||
img.onerror = () =>
|
||||
reject(new Error(t("server.rdpDialog.bitmapDecodeFailed")));
|
||||
img.src = frame.image || "";
|
||||
});
|
||||
};
|
||||
|
||||
const parseRawFrame = async (data: Blob | ArrayBuffer): Promise<RdpRawFrame | null> => {
|
||||
const parseRawFrame = async (
|
||||
data: Blob | ArrayBuffer,
|
||||
): Promise<RdpRawFrame | null> => {
|
||||
const buffer = data instanceof Blob ? await data.arrayBuffer() : data;
|
||||
if (buffer.byteLength < RDP_BINARY_HEADER) return null;
|
||||
const view = new DataView(buffer);
|
||||
@@ -136,13 +287,18 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
String.fromCharCode(view.getUint8(1)) +
|
||||
String.fromCharCode(view.getUint8(2)) +
|
||||
String.fromCharCode(view.getUint8(3));
|
||||
if (magic !== 'RDPR') return null;
|
||||
if (magic !== "RDPR") return null;
|
||||
const x = view.getUint32(4, true);
|
||||
const y = view.getUint32(8, true);
|
||||
const width = view.getUint32(12, true);
|
||||
const height = view.getUint32(16, true);
|
||||
const bytes = width * height * 4;
|
||||
if (width <= 0 || height <= 0 || buffer.byteLength < RDP_BINARY_HEADER + bytes) return null;
|
||||
if (
|
||||
width <= 0 ||
|
||||
height <= 0 ||
|
||||
buffer.byteLength < RDP_BINARY_HEADER + bytes
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
@@ -155,45 +311,70 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
const drawRawFrame = async (data: Blob | ArrayBuffer) => {
|
||||
const frame = await parseRawFrame(data);
|
||||
if (!frame) return;
|
||||
setStatus('');
|
||||
setStatus("");
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (!canvas || !ctx) return;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
patchStats({ received: statsRef.current.received + 1 });
|
||||
try {
|
||||
const pixels = new Uint8ClampedArray(frame.rgba.length);
|
||||
pixels.set(frame.rgba);
|
||||
ctx.putImageData(new ImageData(pixels, frame.width, frame.height), frame.x, frame.y);
|
||||
ctx.putImageData(
|
||||
new ImageData(pixels, frame.width, frame.height),
|
||||
frame.x,
|
||||
frame.y,
|
||||
);
|
||||
setHasFrame(true);
|
||||
patchStats({ drawn: statsRef.current.drawn + 1 });
|
||||
} catch {
|
||||
patchStats({ failed: statsRef.current.failed + 1 });
|
||||
setStatus('RDP 原始帧绘制失败');
|
||||
setStatus(t("server.rdpDialog.rawFrameDrawFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
const drawFrame = async (frame: RdpFrame) => {
|
||||
if (frame.type === 'status' || frame.type === 'error') {
|
||||
setStatus(frame.message || '');
|
||||
if (frame.type === "status" || frame.type === "error") {
|
||||
setStatus(frame.message || "");
|
||||
return;
|
||||
}
|
||||
if (frame.type === 'session') {
|
||||
if (frame.type === "session") {
|
||||
setConnected(true);
|
||||
if (frame.width && frame.height) setCanvasSize({ width: frame.width, height: frame.height });
|
||||
if (frame.width && frame.height)
|
||||
setCanvasSize({ width: frame.width, height: frame.height });
|
||||
return;
|
||||
}
|
||||
if (frame.type === 'clipboard') {
|
||||
if (frame.type === "clipboard") {
|
||||
if (!clipboardEnabled) return;
|
||||
if (frame.text) {
|
||||
setRemoteClipboard(frame.text);
|
||||
await copyToClipboard(frame.text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (frame.type !== 'bitmap' || !frame.image) return;
|
||||
setStatus('');
|
||||
if (frame.type === "rdp_file_download") {
|
||||
if (!fileDownloadEnabled) {
|
||||
setStatus(t("server.sessionPolicy.fileDownloadDisabled"));
|
||||
return;
|
||||
}
|
||||
if (frame.url) {
|
||||
const link = document.createElement("a");
|
||||
link.href = frame.url;
|
||||
link.download = frame.name || "";
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setStatus(
|
||||
t("server.rdpDialog.downloadStarted", { name: frame.name || "file" }),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (frame.type !== "bitmap" || !frame.image) return;
|
||||
setStatus("");
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (!canvas || !ctx) return;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
const received = statsRef.current.received + 1;
|
||||
@@ -204,44 +385,41 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
patchStats({ drawn: statsRef.current.drawn + 1 });
|
||||
} catch {
|
||||
patchStats({ failed: statsRef.current.failed + 1 });
|
||||
setStatus(`RDP 位图绘制失败,已收 ${received} 帧`);
|
||||
setStatus(t("server.rdpDialog.bitmapDrawFailed", { count: received }));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !serverId) return;
|
||||
|
||||
const token = useUserStore.getState().token || localStorage.getItem('token') || '';
|
||||
const maxDisplayWidth = fullscreen ? window.innerWidth : Math.min(window.innerWidth * 0.94, RDP_WIDTH);
|
||||
const maxDisplayHeight = fullscreen ? window.innerHeight - 41 : window.innerHeight - 96;
|
||||
const scale = Math.min(maxDisplayWidth / RDP_WIDTH, maxDisplayHeight / RDP_HEIGHT, 1);
|
||||
const displayWidth = Math.round(RDP_WIDTH * scale);
|
||||
const displayHeight = Math.round(RDP_HEIGHT * scale);
|
||||
// 固定 1024x800 会话尺寸,先排除分辨率变化对画质诊断的干扰。 // Logan.Li
|
||||
const width = RDP_WIDTH;
|
||||
const height = RDP_HEIGHT;
|
||||
setDisplaySize({ width: displayWidth, height: displayHeight });
|
||||
const token =
|
||||
useUserStore.getState().token || localStorage.getItem("token") || "";
|
||||
const { width, height } = calcRdpViewport(false);
|
||||
// 会话分辨率跟随弹窗可用空间,减少大屏下固定 1024x800 造成的左右黑边。 // Logan.Li
|
||||
setDisplaySize({ width, height });
|
||||
setCanvasSize({ width, height });
|
||||
setStatus('正在创建 RDP 会话...');
|
||||
setStatus(t("server.rdpDialog.creating"));
|
||||
setHasFrame(false);
|
||||
setRemoteClipboard('');
|
||||
setRemoteClipboard("");
|
||||
statsRef.current = { received: 0, drawn: 0, failed: 0 };
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (canvas && ctx) {
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
const ws = new WebSocket(`${getWsBase()}/ws/rdp?id=${serverId}&width=${width}&height=${height}&token=${encodeURIComponent(token)}`);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
const ws = new WebSocket(
|
||||
`${getWsBase()}/ws/rdp?id=${serverId}&width=${width}&height=${height}&token=${encodeURIComponent(token)}`,
|
||||
);
|
||||
ws.binaryType = "arraybuffer";
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => setConnected(true);
|
||||
ws.onmessage = (event) => {
|
||||
if (typeof event.data !== 'string') {
|
||||
if (typeof event.data !== "string") {
|
||||
paintQueueRef.current = paintQueueRef.current
|
||||
.catch(() => undefined)
|
||||
.then(() => drawRawFrame(event.data as Blob | ArrayBuffer));
|
||||
@@ -253,13 +431,13 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
.catch(() => undefined)
|
||||
.then(() => drawFrame(frame));
|
||||
} catch {
|
||||
setStatus(String(event.data || '收到无法解析的 RDP 帧'));
|
||||
setStatus(String(event.data || t("server.rdpDialog.unparseableFrame")));
|
||||
}
|
||||
};
|
||||
ws.onerror = () => setStatus('RDP 连接发生错误');
|
||||
ws.onerror = () => setStatus(t("server.rdpDialog.connectionError"));
|
||||
ws.onclose = () => {
|
||||
setConnected(false);
|
||||
setStatus((prev) => prev || 'RDP 连接已关闭');
|
||||
setStatus((prev) => prev || t("server.rdpDialog.connectionClosed"));
|
||||
};
|
||||
|
||||
return () => {
|
||||
@@ -270,16 +448,23 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
setConnected(false);
|
||||
setToolsOpen(false);
|
||||
};
|
||||
}, [open, serverId, fullscreen]);
|
||||
}, [open, serverId]);
|
||||
|
||||
const canvasPoint = (event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const x = Math.round((event.clientX - rect.left) * (canvasSize.width / rect.width));
|
||||
const y = Math.round((event.clientY - rect.top) * (canvasSize.height / rect.height));
|
||||
const x = Math.round(
|
||||
(event.clientX - rect.left) * (canvasSize.width / rect.width),
|
||||
);
|
||||
const y = Math.round(
|
||||
(event.clientY - rect.top) * (canvasSize.height / rect.height),
|
||||
);
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
const showConnectionIssue = Boolean(status && /失败|错误|关闭|无法解析/.test(status));
|
||||
// 状态文本匹配错误关键词(中英双语,兼容 i18n 切换) // Logan.Li
|
||||
const showConnectionIssue = Boolean(
|
||||
status && /失败|错误|关闭|无法解析|fail|error|clos|unpars/i.test(status),
|
||||
);
|
||||
const showCenterLoading = open && !hasFrame && !showConnectionIssue;
|
||||
|
||||
return (
|
||||
@@ -287,19 +472,32 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className={cn(
|
||||
'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0 overflow-hidden',
|
||||
"bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0 overflow-hidden",
|
||||
fullscreen
|
||||
? '!max-w-none !w-screen !h-screen !rounded-none !translate-x-0 !translate-y-0 !left-0 !top-0'
|
||||
: '',
|
||||
? "!max-w-none !w-screen !h-screen !rounded-none !translate-x-0 !translate-y-0 !left-0 !top-0"
|
||||
: "",
|
||||
)}
|
||||
style={fullscreen ? { maxWidth: '100vw', width: '100vw', height: '100vh' } : { maxWidth: '1120px', width: '94vw' }}
|
||||
style={
|
||||
fullscreen
|
||||
? { maxWidth: "100vw", width: "100vw", height: "100vh" }
|
||||
: { maxWidth: displaySize.width, width: displaySize.width }
|
||||
}
|
||||
>
|
||||
<DialogTitle className="sr-only">RDP - {serverName}</DialogTitle>
|
||||
<DialogDescription className="sr-only">Windows RDP 图形会话</DialogDescription>
|
||||
<DialogDescription className="sr-only">
|
||||
{t("server.rdpDialog.graphicSession")}
|
||||
</DialogDescription>
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-[var(--dg-border)]">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className={cn('w-2 h-2 rounded-full', connected ? 'bg-emerald-400' : 'bg-red-400')} />
|
||||
<span className="text-[var(--dg-text-primary)] font-medium">{serverName}</span>
|
||||
<div
|
||||
className={cn(
|
||||
"w-2 h-2 rounded-full",
|
||||
connected ? "bg-emerald-400" : "bg-red-400",
|
||||
)}
|
||||
/>
|
||||
<span className="text-[var(--dg-text-primary)] font-medium">
|
||||
{serverName}
|
||||
</span>
|
||||
<span className="text-[var(--dg-text-muted)]">({serverIp})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -310,25 +508,35 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
type="button"
|
||||
onClick={() => setToolsOpen((v) => !v)}
|
||||
className={cn(
|
||||
'p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]',
|
||||
toolsOpen && 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)]',
|
||||
"p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]",
|
||||
toolsOpen &&
|
||||
"bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)]",
|
||||
)}
|
||||
title="工具"
|
||||
title={t("server.rdpDialog.tools")}
|
||||
>
|
||||
<Wrench className="h-4 w-4" />
|
||||
</button>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent align="end" className="p-0 w-auto border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
|
||||
<PopoverContent
|
||||
align="end"
|
||||
className="p-0 w-auto border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]"
|
||||
>
|
||||
<RdpToolsPanel
|
||||
serverId={serverId}
|
||||
remoteClipboard={remoteClipboard}
|
||||
onSyncToRemote={(text) => {
|
||||
sendEvent({ type: 'clipboard', text });
|
||||
setStatus('已同步到远端');
|
||||
sendEvent({ type: "clipboard", text });
|
||||
setStatus(t("server.rdpDialog.synced"));
|
||||
}}
|
||||
onAutoExec={(command) => {
|
||||
sendEvent({ type: 'auto_exec', text: command });
|
||||
setStatus('正在自动执行上传命令...');
|
||||
onRequestRemoteCopy={requestRemoteCopy}
|
||||
onUploadVirtualFile={(name, text) => {
|
||||
const sent = sendEvent({
|
||||
type: "rdp_file_upload",
|
||||
name,
|
||||
text,
|
||||
});
|
||||
if (!sent) return false;
|
||||
setStatus(t("server.rdpDialog.uploadStaged", { name }));
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
@@ -339,7 +547,11 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
onClick={() => setFullscreen(!fullscreen)}
|
||||
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"
|
||||
>
|
||||
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
|
||||
{fullscreen ? (
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
) : (
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -352,7 +564,11 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
</div>
|
||||
<div
|
||||
className="relative bg-black flex items-center justify-center"
|
||||
style={{ height: fullscreen ? 'calc(100vh - 41px)' : displaySize.height }}
|
||||
style={{
|
||||
height: fullscreen
|
||||
? `calc(100vh - ${RDP_HEADER_HEIGHT}px)`
|
||||
: displaySize.height,
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
@@ -360,19 +576,54 @@ export function RdpDialog({ open, onOpenChange, serverId, serverName, serverIp }
|
||||
height={canvasSize.height}
|
||||
tabIndex={0}
|
||||
className="max-w-full max-h-full outline-none"
|
||||
style={{ width: displaySize.width, height: displaySize.height }}
|
||||
onMouseMove={(e) => sendMouseMove({ type: 'mouse_move', ...canvasPoint(e) })}
|
||||
onMouseDown={(e) => { e.preventDefault(); e.currentTarget.focus(); flushMouseMove(); sendEvent({ type: 'mouse_down', button: e.button, ...canvasPoint(e) }); }}
|
||||
onMouseUp={(e) => { e.preventDefault(); flushMouseMove(); sendEvent({ type: 'mouse_up', button: e.button, ...canvasPoint(e) }); }}
|
||||
onWheel={(e) => { flushMouseMove(); sendEvent({ type: 'wheel', delta_y: Math.trunc(e.deltaY), ...canvasPoint(e) }); }}
|
||||
style={{
|
||||
// 最大化只缩放当前画面,不重连 RDP,避免登录过程闪黑。 // Logan.Li
|
||||
width: fullscreen ? "100%" : displaySize.width,
|
||||
height: fullscreen ? "100%" : displaySize.height,
|
||||
}}
|
||||
onMouseMove={(e) =>
|
||||
sendMouseMove({ type: "mouse_move", ...canvasPoint(e) })
|
||||
}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.focus();
|
||||
flushMouseMove();
|
||||
sendEvent({
|
||||
type: "mouse_down",
|
||||
button: e.button,
|
||||
...canvasPoint(e),
|
||||
});
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
e.preventDefault();
|
||||
flushMouseMove();
|
||||
sendEvent({
|
||||
type: "mouse_up",
|
||||
button: e.button,
|
||||
...canvasPoint(e),
|
||||
});
|
||||
}}
|
||||
onWheel={(e) => {
|
||||
flushMouseMove();
|
||||
sendEvent({
|
||||
type: "wheel",
|
||||
delta_y: Math.trunc(e.deltaY),
|
||||
...canvasPoint(e),
|
||||
});
|
||||
}}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onPaste={(e) => {
|
||||
const text = e.clipboardData.getData('text/plain');
|
||||
if (text) sendEvent({ type: 'clipboard', text });
|
||||
if (!clipboardEnabled) {
|
||||
setStatus(t("server.sessionPolicy.clipboardDisabled"));
|
||||
return;
|
||||
}
|
||||
const text = e.clipboardData.getData("text/plain");
|
||||
if (text) sendEvent({ type: "clipboard", text });
|
||||
}}
|
||||
onKeyDown={(e) => { e.preventDefault(); sendEvent({ type: 'key', code: e.code, key: e.key, down: true }); }}
|
||||
onKeyUp={(e) => { e.preventDefault(); sendEvent({ type: 'key', code: e.code, key: e.key, down: false }); }}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
/>
|
||||
<SessionWatermark />
|
||||
{showCenterLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/25">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-white/80" />
|
||||
|
||||
Reference in new issue
Block a user