|
|
@@ -2,7 +2,7 @@
|
|
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
import api from '@/lib/api';
|
|
|
-import { Server, Play, Square, RotateCw, FileText, Settings, Search, RefreshCw } from 'lucide-react';
|
|
|
+import { Server, Play, RotateCw, FileText, Settings, Search, RefreshCw, CheckCircle2, AlertCircle } from 'lucide-react';
|
|
|
import DockerControl from './DockerControl';
|
|
|
import LogViewer from './LogViewer';
|
|
|
import ConfigManager from './ConfigManager';
|
|
|
@@ -39,7 +39,6 @@ export default function RemoteServerControl() {
|
|
|
const [connecting, setConnecting] = useState(false);
|
|
|
const [connectionError, setConnectionError] = useState<string | null>(null);
|
|
|
|
|
|
- // 获取预配置服务器列表
|
|
|
useEffect(() => {
|
|
|
const fetchServers = async () => {
|
|
|
try {
|
|
|
@@ -47,9 +46,7 @@ export default function RemoteServerControl() {
|
|
|
if (response.data.code === 0) {
|
|
|
const servers = response.data.data.servers || [];
|
|
|
setPreConfiguredServers(servers);
|
|
|
- if (servers.length > 0) {
|
|
|
- setSelectedServerId(servers[0].id);
|
|
|
- }
|
|
|
+ if (servers.length > 0) setSelectedServerId(servers[0].id);
|
|
|
}
|
|
|
} catch (err) {
|
|
|
console.error('获取服务器列表失败:', err);
|
|
|
@@ -61,7 +58,6 @@ export default function RemoteServerControl() {
|
|
|
const handleConnect = async () => {
|
|
|
setConnecting(true);
|
|
|
setConnectionError(null);
|
|
|
-
|
|
|
try {
|
|
|
let response;
|
|
|
if (usePreConfigured) {
|
|
|
@@ -70,12 +66,8 @@ export default function RemoteServerControl() {
|
|
|
setConnecting(false);
|
|
|
return;
|
|
|
}
|
|
|
- // 使用预配置接口测试连接
|
|
|
- response = await api.post('/api/remote/server/docker/status', {
|
|
|
- server_id: selectedServerId
|
|
|
- });
|
|
|
+ response = await api.post('/api/remote/server/docker/status', { server_id: selectedServerId });
|
|
|
} else {
|
|
|
- // 使用手动配置接口测试连接
|
|
|
if (!serverConfig.host || !serverConfig.username) {
|
|
|
setConnectionError('请填写服务器地址和用户名');
|
|
|
setConnecting(false);
|
|
|
@@ -92,44 +84,14 @@ export default function RemoteServerControl() {
|
|
|
setIsConnected(false);
|
|
|
}
|
|
|
} catch (err: any) {
|
|
|
- // ... 错误处理逻辑保持不变 ... console.error('连接错误详情:', err);
|
|
|
- console.error('错误响应:', err.response);
|
|
|
- console.error('错误请求:', err.request);
|
|
|
-
|
|
|
- let errorMsg = '连接失败,请检查服务器配置';
|
|
|
-
|
|
|
+ console.error('连接错误详情:', err);
|
|
|
+ let errorMsg = '连接失败,请检查配置';
|
|
|
if (err.response) {
|
|
|
- // 服务器返回了响应
|
|
|
const status = err.response.status;
|
|
|
- const data = err.response.data;
|
|
|
-
|
|
|
- if (status === 404) {
|
|
|
- errorMsg = `API 路由未找到 (404)。请确认:
|
|
|
-1. 后端服务已重启并加载了新路由
|
|
|
-2. 访问 http://localhost:8888/docs 确认路由是否存在
|
|
|
-3. 检查 web-ui/.env.local 中的 NEXT_PUBLIC_API_URL 是否指向了正确的后端地址 (当前: ${process.env.NEXT_PUBLIC_API_URL})`;
|
|
|
- } else if (status === 401) {
|
|
|
- errorMsg = '未授权 (401),请确认您已登录且有管理员权限';
|
|
|
- } else if (status === 403) {
|
|
|
- errorMsg = '权限不足 (403),需要管理员权限';
|
|
|
- } else if (data?.message) {
|
|
|
- errorMsg = `${data.message} (${status})`;
|
|
|
- } else if (data?.detail) {
|
|
|
- errorMsg = `${data.detail} (${status})`;
|
|
|
- } else {
|
|
|
- errorMsg = `服务器错误 (${status}): ${err.response.statusText}`;
|
|
|
- }
|
|
|
- } else if (err.request) {
|
|
|
- // 请求已发出但没有收到响应
|
|
|
- errorMsg = `无法连接到后端服务器,请检查:
|
|
|
-1. 后端服务是否运行在 http://localhost:8888
|
|
|
-2. 网络连接是否正常
|
|
|
-3. 浏览器控制台是否有 CORS 错误`;
|
|
|
- } else {
|
|
|
- // 其他错误
|
|
|
- errorMsg = err.message || '连接失败,请检查服务器配置';
|
|
|
+ if (status === 404) errorMsg = 'API 路由未找到 (404)';
|
|
|
+ else if (status === 401) errorMsg = '未授权 (401)';
|
|
|
+ else if (err.response.data?.message) errorMsg = err.response.data.message;
|
|
|
}
|
|
|
-
|
|
|
setConnectionError(errorMsg);
|
|
|
setIsConnected(false);
|
|
|
} finally {
|
|
|
@@ -143,233 +105,203 @@ export default function RemoteServerControl() {
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
- <div className="space-y-4 sm:space-y-6">
|
|
|
- {/* 服务器连接配置 */}
|
|
|
- <div className="bg-white rounded-lg shadow-sm border border-slate-200 p-4 sm:p-6">
|
|
|
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
|
|
- <div className="flex items-center gap-2">
|
|
|
- <Server className="text-blue-600" size={24} />
|
|
|
- <h2 className="text-lg sm:text-xl font-semibold text-slate-800">服务器连接配置</h2>
|
|
|
+ <div className="space-y-4 md:space-y-6 p-4 md:p-0">
|
|
|
+
|
|
|
+ {/* === 卡片 1: 服务器连接配置 === */}
|
|
|
+ <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
|
+
|
|
|
+ {/* 卡片头部 */}
|
|
|
+ <div className="p-4 md:p-6 border-b border-slate-100 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ <div className="p-2 bg-blue-50 text-blue-600 rounded-lg">
|
|
|
+ <Server size={20} />
|
|
|
+ </div>
|
|
|
+ <h2 className="text-lg font-bold text-slate-800">服务器连接</h2>
|
|
|
</div>
|
|
|
- <div className="flex bg-slate-100 p-1 rounded-md self-start sm:self-auto">
|
|
|
+
|
|
|
+ {/* 切换开关:移动端全宽 */}
|
|
|
+ <div className="flex bg-slate-100 p-1 rounded-lg w-full md:w-auto">
|
|
|
<button
|
|
|
- onClick={() => {
|
|
|
- if (!isConnected) setUsePreConfigured(true);
|
|
|
- }}
|
|
|
- className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
|
|
- usePreConfigured
|
|
|
- ? 'bg-white text-blue-600 shadow-sm'
|
|
|
- : 'text-slate-600 hover:text-slate-800'
|
|
|
+ onClick={() => !isConnected && setUsePreConfigured(true)}
|
|
|
+ className={`flex-1 md:flex-none px-4 py-2 text-xs font-bold rounded-md transition-all ${
|
|
|
+ usePreConfigured ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500'
|
|
|
}`}
|
|
|
disabled={isConnected}
|
|
|
>
|
|
|
- 预配置服务器
|
|
|
+ 预配置列表
|
|
|
</button>
|
|
|
<button
|
|
|
- onClick={() => {
|
|
|
- if (!isConnected) setUsePreConfigured(false);
|
|
|
- }}
|
|
|
- className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
|
|
- !usePreConfigured
|
|
|
- ? 'bg-white text-blue-600 shadow-sm'
|
|
|
- : 'text-slate-600 hover:text-slate-800'
|
|
|
+ onClick={() => !isConnected && setUsePreConfigured(false)}
|
|
|
+ className={`flex-1 md:flex-none px-4 py-2 text-xs font-bold rounded-md transition-all ${
|
|
|
+ !usePreConfigured ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500'
|
|
|
}`}
|
|
|
disabled={isConnected}
|
|
|
>
|
|
|
- 手动直连
|
|
|
+ 手动输入
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- {usePreConfigured ? (
|
|
|
- <div className="space-y-4">
|
|
|
+ {/* 卡片内容 */}
|
|
|
+ <div className="p-4 md:p-6 space-y-5">
|
|
|
+ {usePreConfigured ? (
|
|
|
<div>
|
|
|
- <label className="block text-sm font-medium text-slate-700 mb-1">选择服务器</label>
|
|
|
- <select
|
|
|
- value={selectedServerId}
|
|
|
- onChange={(e) => setSelectedServerId(e.target.value)}
|
|
|
- className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
|
|
- disabled={isConnected}
|
|
|
- >
|
|
|
- <option value="">-- 请选择服务器 --</option>
|
|
|
- {preConfiguredServers.map((server) => (
|
|
|
- <option key={server.id} value={server.id}>
|
|
|
- {server.name} ({server.host})
|
|
|
- </option>
|
|
|
- ))}
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-slate-700 mb-1 text-xs sm:text-sm">服务器地址 *</label>
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- value={serverConfig.host}
|
|
|
- onChange={(e) => setServerConfig({ ...serverConfig, host: e.target.value })}
|
|
|
- placeholder="192.168.1.100"
|
|
|
- className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
|
|
- disabled={isConnected}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-slate-700 mb-1 text-xs sm:text-sm">SSH端口</label>
|
|
|
- <input
|
|
|
- type="number"
|
|
|
- value={serverConfig.port}
|
|
|
- onChange={(e) => setServerConfig({ ...serverConfig, port: parseInt(e.target.value) || 22 })}
|
|
|
- placeholder="22"
|
|
|
- className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
|
|
- disabled={isConnected}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-slate-700 mb-1 text-xs sm:text-sm">用户名 *</label>
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- value={serverConfig.username}
|
|
|
- onChange={(e) => setServerConfig({ ...serverConfig, username: e.target.value })}
|
|
|
- placeholder="root"
|
|
|
- className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
|
|
- disabled={isConnected}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-slate-700 mb-1 text-xs sm:text-sm">密码</label>
|
|
|
- <input
|
|
|
- type="password"
|
|
|
- value={serverConfig.password || ''}
|
|
|
- onChange={(e) => setServerConfig({ ...serverConfig, password: e.target.value })}
|
|
|
- placeholder="SSH密码"
|
|
|
- className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
|
|
- disabled={isConnected}
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-slate-700 mb-1 text-xs sm:text-sm">私钥文件路径</label>
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- value={serverConfig.key_file || ''}
|
|
|
- onChange={(e) => setServerConfig({ ...serverConfig, key_file: e.target.value })}
|
|
|
- placeholder="/path/to/key.pem"
|
|
|
- className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
|
|
- disabled={isConnected}
|
|
|
- />
|
|
|
+ <label className="block text-xs font-bold text-slate-500 uppercase mb-2">选择目标服务器</label>
|
|
|
+ <div className="relative">
|
|
|
+ <select
|
|
|
+ value={selectedServerId}
|
|
|
+ onChange={(e) => setSelectedServerId(e.target.value)}
|
|
|
+ className="w-full pl-4 pr-10 py-3 border border-slate-300 rounded-xl appearance-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm bg-white"
|
|
|
+ disabled={isConnected}
|
|
|
+ >
|
|
|
+ <option value="">-- 请选择服务器 --</option>
|
|
|
+ {preConfiguredServers.map((server) => (
|
|
|
+ <option key={server.id} value={server.id}>
|
|
|
+ {server.name} ({server.host})
|
|
|
+ </option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ <div className="absolute right-3 top-3.5 text-slate-400 pointer-events-none">
|
|
|
+ <Search size={16} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
-
|
|
|
- <div>
|
|
|
- <label className="block text-sm font-medium text-slate-700 mb-1 text-xs sm:text-sm">项目路径</label>
|
|
|
- <input
|
|
|
- type="text"
|
|
|
- value={serverConfig.project_path}
|
|
|
- onChange={(e) => setServerConfig({ ...serverConfig, project_path: e.target.value })}
|
|
|
- placeholder="/root/troov-asyncio"
|
|
|
- className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
|
|
- disabled={isConnected}
|
|
|
- />
|
|
|
+ ) : (
|
|
|
+ // 手动输入模式:移动端单列,桌面端双列
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
+ <div className="space-y-1">
|
|
|
+ <label className="text-xs font-bold text-slate-500">IP 地址</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={serverConfig.host}
|
|
|
+ onChange={(e) => setServerConfig({ ...serverConfig, host: e.target.value })}
|
|
|
+ placeholder="192.168.1.100"
|
|
|
+ className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
|
|
+ disabled={isConnected}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-1">
|
|
|
+ <label className="text-xs font-bold text-slate-500">端口</label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ value={serverConfig.port}
|
|
|
+ onChange={(e) => setServerConfig({ ...serverConfig, port: parseInt(e.target.value) || 22 })}
|
|
|
+ className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
|
|
+ disabled={isConnected}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ {/* 其他字段同理... */}
|
|
|
+ <div className="space-y-1">
|
|
|
+ <label className="text-xs font-bold text-slate-500">用户名</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={serverConfig.username}
|
|
|
+ onChange={(e) => setServerConfig({ ...serverConfig, username: e.target.value })}
|
|
|
+ placeholder="root"
|
|
|
+ className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
|
|
+ disabled={isConnected}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-1">
|
|
|
+ <label className="text-xs font-bold text-slate-500">密码</label>
|
|
|
+ <input
|
|
|
+ type="password"
|
|
|
+ value={serverConfig.password || ''}
|
|
|
+ onChange={(e) => setServerConfig({ ...serverConfig, password: e.target.value })}
|
|
|
+ className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
|
|
+ disabled={isConnected}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- <div className="mt-4 space-y-2">
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 状态反馈区 */}
|
|
|
{connectionError && (
|
|
|
- <div className="bg-red-50 border border-red-200 text-red-700 px-3 sm:px-4 py-2 rounded-md text-xs sm:text-sm overflow-x-auto whitespace-pre-wrap">
|
|
|
- {connectionError}
|
|
|
+ <div className="bg-red-50 border border-red-100 text-red-700 px-4 py-3 rounded-lg text-sm flex items-start gap-2">
|
|
|
+ <AlertCircle size={18} className="shrink-0 mt-0.5" />
|
|
|
+ <span className="break-all">{connectionError}</span>
|
|
|
</div>
|
|
|
)}
|
|
|
{isConnected && (
|
|
|
- <div className="bg-green-50 border border-green-200 text-green-700 px-3 sm:px-4 py-2 rounded-md text-xs sm:text-sm flex items-center gap-2">
|
|
|
- <Server size={16} />
|
|
|
- 已连接到服务器
|
|
|
+ <div className="bg-green-50 border border-green-100 text-green-700 px-4 py-3 rounded-lg text-sm flex items-center gap-2 font-medium">
|
|
|
+ <CheckCircle2 size={18} />
|
|
|
+ 已成功连接到服务器
|
|
|
</div>
|
|
|
)}
|
|
|
- <div className="flex flex-wrap gap-2">
|
|
|
+
|
|
|
+ {/* 操作按钮:移动端垂直排列全宽,桌面端水平 */}
|
|
|
+ <div className="flex flex-col sm:flex-row gap-3 pt-2">
|
|
|
{!isConnected ? (
|
|
|
<>
|
|
|
<button
|
|
|
onClick={handleConnect}
|
|
|
disabled={connecting}
|
|
|
- className="px-4 py-2 bg-blue-600 text-white text-sm sm:text-base rounded-md hover:bg-blue-700 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
+ className="w-full sm:w-auto px-6 py-3 bg-blue-600 text-white text-sm font-bold rounded-xl hover:bg-blue-700 transition-all flex justify-center items-center gap-2 disabled:opacity-70 active:scale-95 shadow-md shadow-blue-100"
|
|
|
>
|
|
|
- <Server size={18} className={connecting ? 'animate-pulse' : ''} />
|
|
|
- {connecting ? '连接中...' : '连接服务器'}
|
|
|
+ {connecting ? <RefreshCw size={18} className="animate-spin" /> : <Server size={18} />}
|
|
|
+ {connecting ? '正在连接...' : '立即连接'}
|
|
|
</button>
|
|
|
<button
|
|
|
onClick={async () => {
|
|
|
try {
|
|
|
const res = await api.get('/api/ping');
|
|
|
- alert(`API 测试成功: ${JSON.stringify(res.data)}`);
|
|
|
+ alert(`API OK: ${JSON.stringify(res.data)}`);
|
|
|
} catch (err: any) {
|
|
|
- alert(`API 测试失败: ${err.response?.status || '无响应'} - ${err.response?.data?.message || err.message}`);
|
|
|
+ alert(`API Error: ${err.message}`);
|
|
|
}
|
|
|
}}
|
|
|
- className="px-4 py-2 bg-gray-600 text-white text-sm sm:text-base rounded-md hover:bg-gray-700 transition-colors flex items-center gap-2"
|
|
|
- title="测试后端 API 连接"
|
|
|
+ className="w-full sm:w-auto px-6 py-3 bg-slate-100 text-slate-600 text-sm font-bold rounded-xl hover:bg-slate-200 transition-all flex justify-center items-center gap-2 active:scale-95"
|
|
|
>
|
|
|
- <RefreshCw size={18} />
|
|
|
- 测试 API
|
|
|
+ <RefreshCw size={18} /> 测试 API
|
|
|
</button>
|
|
|
</>
|
|
|
) : (
|
|
|
<button
|
|
|
onClick={handleDisconnect}
|
|
|
- className="px-4 py-2 bg-red-600 text-white text-sm sm:text-base rounded-md hover:bg-red-700 transition-colors flex items-center gap-2"
|
|
|
+ className="w-full sm:w-auto px-6 py-3 bg-red-50 text-red-600 border border-red-100 text-sm font-bold rounded-xl hover:bg-red-100 transition-all flex justify-center items-center gap-2 active:scale-95"
|
|
|
>
|
|
|
- <Server size={18} />
|
|
|
- 断开连接
|
|
|
+ <Server size={18} /> 断开连接
|
|
|
</button>
|
|
|
)}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- {/* 功能标签页 */}
|
|
|
+ {/* === 卡片 2: 功能控制区 (连接后显示) === */}
|
|
|
{isConnected && (
|
|
|
- <div className="bg-white rounded-lg shadow-sm border border-slate-200 overflow-hidden">
|
|
|
- <div className="border-b border-slate-200 overflow-x-auto">
|
|
|
- <nav className="flex -mb-px min-w-max">
|
|
|
+ <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden animate-in fade-in slide-in-from-bottom-4">
|
|
|
+
|
|
|
+ {/* Tab 导航:移动端支持横向滚动 */}
|
|
|
+ <div className="border-b border-slate-100 overflow-x-auto scrollbar-hide">
|
|
|
+ <nav className="flex min-w-max px-2">
|
|
|
<button
|
|
|
onClick={() => setActiveTab('docker')}
|
|
|
- className={`px-4 sm:px-6 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2 ${
|
|
|
- activeTab === 'docker'
|
|
|
- ? 'border-blue-600 text-blue-600'
|
|
|
- : 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
|
|
+ className={`px-4 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${
|
|
|
+ activeTab === 'docker' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
|
|
}`}
|
|
|
>
|
|
|
- <Play size={18} />
|
|
|
- Docker控制
|
|
|
+ <Play size={18} /> Docker 控制
|
|
|
</button>
|
|
|
<button
|
|
|
onClick={() => setActiveTab('logs')}
|
|
|
- className={`px-4 sm:px-6 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2 ${
|
|
|
- activeTab === 'logs'
|
|
|
- ? 'border-blue-600 text-blue-600'
|
|
|
- : 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
|
|
+ className={`px-4 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${
|
|
|
+ activeTab === 'logs' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
|
|
}`}
|
|
|
>
|
|
|
- <FileText size={18} />
|
|
|
- 日志查看
|
|
|
+ <FileText size={18} /> 日志查看
|
|
|
</button>
|
|
|
<button
|
|
|
onClick={() => setActiveTab('config')}
|
|
|
- className={`px-4 sm:px-6 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2 ${
|
|
|
- activeTab === 'config'
|
|
|
- ? 'border-blue-600 text-blue-600'
|
|
|
- : 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
|
|
|
+ className={`px-4 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${
|
|
|
+ activeTab === 'config' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
|
|
|
}`}
|
|
|
>
|
|
|
- <Settings size={18} />
|
|
|
- 配置文件
|
|
|
+ <Settings size={18} /> 配置文件
|
|
|
</button>
|
|
|
</nav>
|
|
|
</div>
|
|
|
|
|
|
- <div className="p-4 sm:p-6">
|
|
|
+ <div className="p-4 md:p-6">
|
|
|
{activeTab === 'docker' && (
|
|
|
<DockerControl
|
|
|
serverConfig={serverConfig}
|
|
|
@@ -393,4 +325,4 @@ export default function RemoteServerControl() {
|
|
|
)}
|
|
|
</div>
|
|
|
);
|
|
|
-}
|
|
|
+}
|