feat: Docker部署 + 云主机导入 + 终端优化
Docker 部署: - 新增根目录 docker-compose.yml(MySQL+Redis+前后端四容器) - 新增 frontend/Dockerfile(多阶段构建)和 nginx.conf(SPA+API反代) - 修复 backend/Dockerfile:GOPROXY国内加速、编译整个cmd包 云主机导入功能: - 新增 backend/internal/cloud 包:Provider接口 + 阿里云API客户端 - 新增 cloud_sync.go:区域同步/主机发现/导入 API - 重写 CloudServerImport.tsx:4步流程(选云→选账号→选区域→发现导入) - 修复导入外键约束失败(region_id/cloud_id 设为 NULL) 终端优化: - 修复双X关闭按钮重影(showCloseButton=false) - 修复最大化→最小化不恢复(ResizeObserver + 禁用CSS过渡) 其他: - GORM AutoMigrate 逐表迁移,避免索引冲突阻塞启动 - Makefile 更新为 AIOps + 新增 docker 命令组 - .env.example 移至 backend 目录
This commit is contained in:
1 parent
2e175daf80
commit
d0344c2f52
14 files changed
+1265
-200
No files matched your search
@@ -86,7 +86,6 @@ export function TerminalDialog({
|
||||
|
||||
ws.onopen = () => {
|
||||
setConnected(true);
|
||||
// 挂载终端到容器
|
||||
if (containerRef.current) {
|
||||
term.open(containerRef.current);
|
||||
requestAnimationFrame(() => {
|
||||
@@ -107,22 +106,7 @@ export function TerminalDialog({
|
||||
term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n');
|
||||
};
|
||||
|
||||
// resize 监听
|
||||
const handleResize = () => {
|
||||
if (fitRef.current && termRef.current) {
|
||||
try {
|
||||
fit.fit();
|
||||
const { cols, rows } = termRef.current;
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ status: 1, cols, rows }));
|
||||
}
|
||||
} catch { /* noop */ }
|
||||
}
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
try {
|
||||
ws.close();
|
||||
term.dispose();
|
||||
@@ -133,24 +117,51 @@ export function TerminalDialog({
|
||||
};
|
||||
}, [open, serverId]);
|
||||
|
||||
// fullscreen 切换时重新 fit
|
||||
// 用 ResizeObserver 监听容器实际尺寸变化,自动 fit 终端
|
||||
// 这比 window resize 或 setTimeout 更可靠:
|
||||
// - 全屏/窗口切换时容器尺寸在 CSS 过渡期间持续变化,ResizeObserver 每次都会触发
|
||||
// - 不需要猜测延迟时间
|
||||
useEffect(() => {
|
||||
if (open && fitRef.current) {
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
fitRef.current?.fit();
|
||||
termRef.current?.focus();
|
||||
} catch { /* noop */ }
|
||||
});
|
||||
}
|
||||
}, [fullscreen, open]);
|
||||
if (!open) return;
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
let resizeTimer: ReturnType<typeof setTimeout>;
|
||||
const ro = new ResizeObserver(() => {
|
||||
// 防抖:CSS 过渡期间会连续触发多次,合并为最后一次
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
if (fitRef.current && termRef.current && wsRef.current) {
|
||||
try {
|
||||
fitRef.current.fit();
|
||||
termRef.current.focus();
|
||||
const { cols, rows } = termRef.current;
|
||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify({ status: 1, cols, rows }));
|
||||
}
|
||||
} catch { /* noop */ }
|
||||
}
|
||||
}, 60);
|
||||
});
|
||||
|
||||
ro.observe(container);
|
||||
return () => {
|
||||
clearTimeout(resizeTimer);
|
||||
ro.disconnect();
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
className={cn(
|
||||
'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0',
|
||||
fullscreen && '!max-w-none !w-screen !h-screen !rounded-none !translate-x-0 !translate-y-0 !left-0 !top-0'
|
||||
// 禁用 CSS 过渡:终端窗口应该即时缩放,不需要动画
|
||||
'!duration-0 !transition-none',
|
||||
fullscreen
|
||||
? '!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: '900px', width: '90vw' }}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user