| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- 'use client';
- import { useState, useEffect } from 'react';
- import api from '@/lib/api';
- 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';
- interface ServerConfig {
- host: string;
- port: number;
- username: string;
- password?: string;
- key_file?: string;
- project_path: string;
- }
- interface PreConfiguredServer {
- id: string;
- name: string;
- host: string;
- }
- export default function RemoteServerControl() {
- const [activeTab, setActiveTab] = useState<'docker' | 'logs' | 'config'>('docker');
- const [usePreConfigured, setUsePreConfigured] = useState(true);
- const [preConfiguredServers, setPreConfiguredServers] = useState<PreConfiguredServer[]>([]);
- const [selectedServerId, setSelectedServerId] = useState<string>('');
- const [serverConfig, setServerConfig] = useState<ServerConfig>({
- host: '',
- port: 22,
- username: 'root',
- password: '',
- key_file: '',
- project_path: '/root/troov-asyncio',
- });
- const [isConnected, setIsConnected] = useState(false);
- const [connecting, setConnecting] = useState(false);
- const [connectionError, setConnectionError] = useState<string | null>(null);
- useEffect(() => {
- const fetchServers = async () => {
- try {
- const response = await api.get('/api/remote/servers');
- if (response.data.code === 0) {
- const servers = response.data.data.servers || [];
- setPreConfiguredServers(servers);
- if (servers.length > 0) setSelectedServerId(servers[0].id);
- }
- } catch (err) {
- console.error('获取服务器列表失败:', err);
- }
- };
- fetchServers();
- }, []);
- const handleConnect = async () => {
- setConnecting(true);
- setConnectionError(null);
- try {
- let response;
- if (usePreConfigured) {
- if (!selectedServerId) {
- setConnectionError('请选择服务器');
- setConnecting(false);
- return;
- }
- response = await api.post('/api/remote/server/docker/status', { server_id: selectedServerId });
- } else {
- if (!serverConfig.host || !serverConfig.username) {
- setConnectionError('请填写服务器地址和用户名');
- setConnecting(false);
- return;
- }
- response = await api.post('/api/remote/docker/status', serverConfig);
- }
-
- if (response.data.code === 0) {
- setIsConnected(true);
- setConnectionError(null);
- } else {
- setConnectionError(response.data.message || '连接失败');
- setIsConnected(false);
- }
- } catch (err: any) {
- console.error('连接错误详情:', err);
- let errorMsg = '连接失败,请检查配置';
- if (err.response) {
- const status = err.response.status;
- 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 {
- setConnecting(false);
- }
- };
- const handleDisconnect = () => {
- setIsConnected(false);
- setConnectionError(null);
- };
- return (
- <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-lg w-full md:w-auto">
- <button
- 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={() => !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>
-
- {/* 卡片内容 */}
- <div className="p-4 md:p-6 space-y-5">
- {usePreConfigured ? (
- <div>
- <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 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>
- )}
-
- {/* 状态反馈区 */}
- {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-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-col sm:flex-row gap-3 pt-2">
- {!isConnected ? (
- <>
- <button
- onClick={handleConnect}
- disabled={connecting}
- 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"
- >
- {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 OK: ${JSON.stringify(res.data)}`);
- } catch (err: any) {
- alert(`API Error: ${err.message}`);
- }
- }}
- 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
- </button>
- </>
- ) : (
- <button
- onClick={handleDisconnect}
- 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} /> 断开连接
- </button>
- )}
- </div>
- </div>
- </div>
- {/* === 卡片 2: 功能控制区 (连接后显示) === */}
- {isConnected && (
- <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 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 控制
- </button>
- <button
- onClick={() => setActiveTab('logs')}
- 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} /> 日志查看
- </button>
- <button
- onClick={() => setActiveTab('config')}
- 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} /> 配置文件
- </button>
- </nav>
- </div>
-
- <div className="p-4 md:p-6">
- {activeTab === 'docker' && (
- <DockerControl
- serverConfig={serverConfig}
- serverId={usePreConfigured ? selectedServerId : undefined}
- />
- )}
- {activeTab === 'logs' && (
- <LogViewer
- serverConfig={serverConfig}
- serverId={usePreConfigured ? selectedServerId : undefined}
- />
- )}
- {activeTab === 'config' && (
- <ConfigManager
- serverConfig={serverConfig}
- serverId={usePreConfigured ? selectedServerId : undefined}
- />
- )}
- </div>
- </div>
- )}
- </div>
- );
- }
|