From 5b6a06016ec8f3b1bb5b005bc216f0e317f8bc58 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Sat, 6 Dec 2025 17:01:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9SSH=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../List/components/SSHTerminalContent.tsx | 66 +++++++++---------- .../List/components/SSHWindowManager.tsx | 7 +- frontend/src/router/RootRedirect.tsx | 29 ++++++++ frontend/src/router/index.tsx | 11 ++-- frontend/src/utils/request.ts | 9 ++- 5 files changed, 74 insertions(+), 48 deletions(-) create mode 100644 frontend/src/router/RootRedirect.tsx diff --git a/frontend/src/pages/Resource/Server/List/components/SSHTerminalContent.tsx b/frontend/src/pages/Resource/Server/List/components/SSHTerminalContent.tsx index b7b41b2a..84f50b45 100644 --- a/frontend/src/pages/Resource/Server/List/components/SSHTerminalContent.tsx +++ b/frontend/src/pages/Resource/Server/List/components/SSHTerminalContent.tsx @@ -40,13 +40,13 @@ interface SSHTerminalContentProps { onStatusChange?: (status: ConnectionStatus) => void; // 状态变化回调 } -type ConnectionStatus = 'initializing' | 'connecting' | 'connected' | 'disconnecting' | 'disconnected' | 'error'; +type ConnectionStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'error'; interface WebSocketMessage { - type: 'output' | 'error' | 'status'; - data?: string; - message?: string; - status?: ConnectionStatus; + type: 'output' | 'input' | 'status' | 'error'; + data: string; + timestamp?: number; + metadata?: Record | null; } export const SSHTerminalContent: React.FC = ({ @@ -65,7 +65,7 @@ export const SSHTerminalContent: React.FC = ({ const onStatusChangeRef = useRef(onStatusChange); const isClosingRef = useRef(false); - const [connectionStatus, setConnectionStatus] = useState('initializing'); + const [connectionStatus, setConnectionStatus] = useState('connecting'); const [errorMessage, setErrorMessage] = useState(''); const [fontSize, setFontSize] = useState(14); const [showSearch, setShowSearch] = useState(false); @@ -158,7 +158,7 @@ export const SSHTerminalContent: React.FC = ({ } } - setConnectionStatus('initializing'); + setConnectionStatus('connecting'); setErrorMessage(''); const terminal = new Terminal({ @@ -275,27 +275,26 @@ export const SSHTerminalContent: React.FC = ({ break; case 'error': - console.error('❌ SSH错误:', msg.message); - terminalInstanceRef.current?.writeln(`\r\n\x1b[31m错误: ${msg.message}\x1b[0m\r\n`); - message.error(msg.message || '连接错误'); + console.error('❌ SSH错误:', msg.data); + terminalInstanceRef.current?.writeln(`\r\n\x1b[31m错误: ${msg.data}\x1b[0m\r\n`); + message.error(msg.data || '连接错误'); break; case 'status': - console.log('📊 状态变化:', msg.status); - if (msg.status) { - setConnectionStatus(msg.status); - if (msg.status === 'connected') { - // 显示审计警告 - terminalInstanceRef.current?.writeln('\r\n\x1b[33m┌────────────────────────────────────────────────────────────┐\x1b[0m'); - terminalInstanceRef.current?.writeln('\x1b[33m│ ⚠️ 安全提示:本次SSH会话将被全程审计记录 │\x1b[0m'); - terminalInstanceRef.current?.writeln('\x1b[33m│ • 所有操作命令、输入、输出都将被完整记录 │\x1b[0m'); - terminalInstanceRef.current?.writeln('\x1b[33m│ • 审计日志用于安全审查、故障排查和合规要求 │\x1b[0m'); - terminalInstanceRef.current?.writeln('\x1b[33m│ • 请规范操作,遵守企业信息安全管理制度 │\x1b[0m'); - terminalInstanceRef.current?.writeln('\x1b[33m└────────────────────────────────────────────────────────────┘\x1b[0m\r\n'); - setTimeout(() => { - fitAddonRef.current?.fit(); - }, 100); - } + console.log('📊 状态变化:', msg.data); + setConnectionStatus(msg.data as ConnectionStatus); + if (msg.data === 'connected') { + // 显示审计警告 + terminalInstanceRef.current?.writeln('\r\n\x1b[33m┌─────────────────────────────────────────────────────────────\x1b[0m'); + terminalInstanceRef.current?.writeln('\x1b[33m│ ⚠️ 链宇技术有限公司 - 安全提示\x1b[0m'); + terminalInstanceRef.current?.writeln('\x1b[33m│ 本次SSH会话将被全程审计记录\x1b[0m'); + terminalInstanceRef.current?.writeln('\x1b[33m│ • 所有操作命令、输入、输出都将被完整记录\x1b[0m'); + terminalInstanceRef.current?.writeln('\x1b[33m│ • 审计日志用于安全审查、故障排查和合规要求\x1b[0m'); + terminalInstanceRef.current?.writeln('\x1b[33m│ • 请规范操作,遵守企业信息安全管理制度\x1b[0m'); + terminalInstanceRef.current?.writeln('\x1b[33m└─────────────────────────────────────────────────────────────\x1b[0m\r\n'); + setTimeout(() => { + fitAddonRef.current?.fit(); + }, 100); } break; } @@ -376,7 +375,6 @@ export const SSHTerminalContent: React.FC = ({ const gracefulClose = useCallback(() => { console.log('🚪 开始优雅关闭 SSH连接'); isClosingRef.current = true; - setConnectionStatus('disconnecting'); terminalInstanceRef.current?.writeln('\r\n\x1b[33m正在断开连接...\x1b[0m'); if (wsRef.current) { @@ -574,14 +572,12 @@ export const SSHTerminalContent: React.FC = ({ const getStatusBadge = () => { switch (connectionStatus) { - case 'initializing': - return 初始化中; case 'connecting': return 连接中; case 'connected': return
已连接; - case 'disconnecting': - return 断开中; + case 'reconnecting': + return 重连中; case 'error': return 连接失败; case 'disconnected': @@ -721,17 +717,17 @@ export const SSHTerminalContent: React.FC = ({ className="w-full h-full p-2 ssh-terminal-content" /> - {connectionStatus === 'initializing' && ( -
+ {connectionStatus === 'connecting' && ( +
-

初始化SSH终端...

+

正在连接SSH...

)} - {connectionStatus === 'disconnecting' && ( + {connectionStatus === 'reconnecting' && (
-

正在断开连接...

+

连接中断,正在重连...

)} diff --git a/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx b/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx index 3b1cdcbc..a5937c11 100644 --- a/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx +++ b/frontend/src/pages/Resource/Server/List/components/SSHWindowManager.tsx @@ -10,7 +10,7 @@ export interface SSHWindow { isMinimized: boolean; position: { x: number; y: number }; size: { width: number; height: number }; - connectionStatus?: 'initializing' | 'connecting' | 'connected' | 'disconnecting' | 'disconnected' | 'error'; + connectionStatus?: 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'error'; } interface SSHWindowManagerProps { @@ -32,7 +32,7 @@ export const SSHWindowManager: React.FC = ({ onOpenWindow isMinimized: false, position: { x: 100 + offset, y: 100 + offset }, size: { width: 1200, height: 700 }, - connectionStatus: 'initializing', + connectionStatus: 'connecting', }; setWindows(prev => [...prev, newWindow]); @@ -123,9 +123,8 @@ export const SSHWindowManager: React.FC = ({ onOpenWindow case 'disconnected': return 'bg-red-600 hover:bg-red-700'; case 'connecting': - case 'initializing': return 'bg-yellow-600 hover:bg-yellow-700'; - case 'disconnecting': + case 'reconnecting': return 'bg-orange-600 hover:bg-orange-700'; default: return 'bg-blue-600 hover:bg-blue-700'; diff --git a/frontend/src/router/RootRedirect.tsx b/frontend/src/router/RootRedirect.tsx new file mode 100644 index 00000000..4cded3f9 --- /dev/null +++ b/frontend/src/router/RootRedirect.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import type { RootState } from '@/store'; + +/** + * 根路径智能重定向组件 + * 根据用户登录状态和菜单权限进行不同的跳转: + * 1. 未登录 -> 登录页 + * 2. 已登录 + 有菜单 -> Dashboard + * 3. 已登录 + 无菜单 -> 无权限页面 + */ +const RootRedirect: React.FC = () => { + const token = useSelector((state: RootState) => state.user.token); + const menus = useSelector((state: RootState) => state.user.menus); + const hasMenus = menus && menus.length > 0; + + // 未登录,跳转到登录页 + if (!token) { + return ; + } + + // 已登录,根据菜单权限跳转 + return hasMenus + ? + : ; +}; + +export default RootRedirect; diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 5bf27bde..b9d46566 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -8,6 +8,7 @@ import store from '../store'; import ProtectedRoute from './ProtectedRoute'; import RouteLoading from '@/components/RouteLoading'; import ErrorFallback from '@/components/ErrorFallback'; +import RootRedirect from './RootRedirect'; // 错误页面 const Forbidden = lazy(() => import('@/pages/Error/403')); @@ -97,7 +98,7 @@ export const createDynamicRouter = () => { children: [ { path: '', - element: hasMenus ? : , + element: , }, // 写死的测试路由:表单设计器测试页面 { @@ -116,9 +117,11 @@ export const createDynamicRouter = () => { { path: 'no-permission', element: ( - }> - - + + }> + + + ), }, // 403 无权限页面 diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 28697dc2..b0bcc9df 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -2,6 +2,8 @@ import axios, {AxiosRequestConfig, AxiosResponse} from 'axios'; import {toast} from '@/components/ui/use-toast'; import { API_BASE_URL, isDev } from '@/config/env'; import { maintenanceDetector } from '@/services/maintenanceDetector'; +import store from '../store'; +import { logout } from '../store/userSlice'; export interface Response { code: number; @@ -100,11 +102,8 @@ const errorHandler = (error: any) => { variant: 'destructive' }); - // 清除本地存储的所有用户相关信息 - localStorage.removeItem('token'); - localStorage.removeItem('userInfo'); - localStorage.removeItem('menus'); - localStorage.removeItem('tenantId'); + // 使用统一的 logout action 清除所有用户数据 + store.dispatch(logout()); // 延迟跳转,确保提示能显示出来 setTimeout(() => {