RemoteServerControl.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. 'use client';
  2. import { useState, useEffect } from 'react';
  3. import api from '@/lib/api';
  4. import { Server, Play, RotateCw, FileText, Settings, Search, RefreshCw, CheckCircle2, AlertCircle } from 'lucide-react';
  5. import DockerControl from './DockerControl';
  6. import LogViewer from './LogViewer';
  7. import ConfigManager from './ConfigManager';
  8. interface ServerConfig {
  9. host: string;
  10. port: number;
  11. username: string;
  12. password?: string;
  13. key_file?: string;
  14. project_path: string;
  15. }
  16. interface PreConfiguredServer {
  17. id: string;
  18. name: string;
  19. host: string;
  20. }
  21. export default function RemoteServerControl() {
  22. const [activeTab, setActiveTab] = useState<'docker' | 'logs' | 'config'>('docker');
  23. const [usePreConfigured, setUsePreConfigured] = useState(true);
  24. const [preConfiguredServers, setPreConfiguredServers] = useState<PreConfiguredServer[]>([]);
  25. const [selectedServerId, setSelectedServerId] = useState<string>('');
  26. const [serverConfig, setServerConfig] = useState<ServerConfig>({
  27. host: '',
  28. port: 22,
  29. username: 'root',
  30. password: '',
  31. key_file: '',
  32. project_path: '/root/troov-asyncio',
  33. });
  34. const [isConnected, setIsConnected] = useState(false);
  35. const [connecting, setConnecting] = useState(false);
  36. const [connectionError, setConnectionError] = useState<string | null>(null);
  37. useEffect(() => {
  38. const fetchServers = async () => {
  39. try {
  40. const response = await api.get('/api/remote/servers');
  41. if (response.data.code === 0) {
  42. const servers = response.data.data.servers || [];
  43. setPreConfiguredServers(servers);
  44. if (servers.length > 0) setSelectedServerId(servers[0].id);
  45. }
  46. } catch (err) {
  47. console.error('获取服务器列表失败:', err);
  48. }
  49. };
  50. fetchServers();
  51. }, []);
  52. const handleConnect = async () => {
  53. setConnecting(true);
  54. setConnectionError(null);
  55. try {
  56. let response;
  57. if (usePreConfigured) {
  58. if (!selectedServerId) {
  59. setConnectionError('请选择服务器');
  60. setConnecting(false);
  61. return;
  62. }
  63. response = await api.post('/api/remote/server/docker/status', { server_id: selectedServerId });
  64. } else {
  65. if (!serverConfig.host || !serverConfig.username) {
  66. setConnectionError('请填写服务器地址和用户名');
  67. setConnecting(false);
  68. return;
  69. }
  70. response = await api.post('/api/remote/docker/status', serverConfig);
  71. }
  72. if (response.data.code === 0) {
  73. setIsConnected(true);
  74. setConnectionError(null);
  75. } else {
  76. setConnectionError(response.data.message || '连接失败');
  77. setIsConnected(false);
  78. }
  79. } catch (err: any) {
  80. console.error('连接错误详情:', err);
  81. let errorMsg = '连接失败,请检查配置';
  82. if (err.response) {
  83. const status = err.response.status;
  84. if (status === 404) errorMsg = 'API 路由未找到 (404)';
  85. else if (status === 401) errorMsg = '未授权 (401)';
  86. else if (err.response.data?.message) errorMsg = err.response.data.message;
  87. }
  88. setConnectionError(errorMsg);
  89. setIsConnected(false);
  90. } finally {
  91. setConnecting(false);
  92. }
  93. };
  94. const handleDisconnect = () => {
  95. setIsConnected(false);
  96. setConnectionError(null);
  97. };
  98. return (
  99. <div className="space-y-4 md:space-y-6 p-4 md:p-0">
  100. {/* === 卡片 1: 服务器连接配置 === */}
  101. <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
  102. {/* 卡片头部 */}
  103. <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">
  104. <div className="flex items-center gap-3">
  105. <div className="p-2 bg-blue-50 text-blue-600 rounded-lg">
  106. <Server size={20} />
  107. </div>
  108. <h2 className="text-lg font-bold text-slate-800">服务器连接</h2>
  109. </div>
  110. {/* 切换开关:移动端全宽 */}
  111. <div className="flex bg-slate-100 p-1 rounded-lg w-full md:w-auto">
  112. <button
  113. onClick={() => !isConnected && setUsePreConfigured(true)}
  114. className={`flex-1 md:flex-none px-4 py-2 text-xs font-bold rounded-md transition-all ${
  115. usePreConfigured ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500'
  116. }`}
  117. disabled={isConnected}
  118. >
  119. 预配置列表
  120. </button>
  121. <button
  122. onClick={() => !isConnected && setUsePreConfigured(false)}
  123. className={`flex-1 md:flex-none px-4 py-2 text-xs font-bold rounded-md transition-all ${
  124. !usePreConfigured ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500'
  125. }`}
  126. disabled={isConnected}
  127. >
  128. 手动输入
  129. </button>
  130. </div>
  131. </div>
  132. {/* 卡片内容 */}
  133. <div className="p-4 md:p-6 space-y-5">
  134. {usePreConfigured ? (
  135. <div>
  136. <label className="block text-xs font-bold text-slate-500 uppercase mb-2">选择目标服务器</label>
  137. <div className="relative">
  138. <select
  139. value={selectedServerId}
  140. onChange={(e) => setSelectedServerId(e.target.value)}
  141. 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"
  142. disabled={isConnected}
  143. >
  144. <option value="">-- 请选择服务器 --</option>
  145. {preConfiguredServers.map((server) => (
  146. <option key={server.id} value={server.id}>
  147. {server.name} ({server.host})
  148. </option>
  149. ))}
  150. </select>
  151. <div className="absolute right-3 top-3.5 text-slate-400 pointer-events-none">
  152. <Search size={16} />
  153. </div>
  154. </div>
  155. </div>
  156. ) : (
  157. // 手动输入模式:移动端单列,桌面端双列
  158. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  159. <div className="space-y-1">
  160. <label className="text-xs font-bold text-slate-500">IP 地址</label>
  161. <input
  162. type="text"
  163. value={serverConfig.host}
  164. onChange={(e) => setServerConfig({ ...serverConfig, host: e.target.value })}
  165. placeholder="192.168.1.100"
  166. 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"
  167. disabled={isConnected}
  168. />
  169. </div>
  170. <div className="space-y-1">
  171. <label className="text-xs font-bold text-slate-500">端口</label>
  172. <input
  173. type="number"
  174. value={serverConfig.port}
  175. onChange={(e) => setServerConfig({ ...serverConfig, port: parseInt(e.target.value) || 22 })}
  176. 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"
  177. disabled={isConnected}
  178. />
  179. </div>
  180. {/* 其他字段同理... */}
  181. <div className="space-y-1">
  182. <label className="text-xs font-bold text-slate-500">用户名</label>
  183. <input
  184. type="text"
  185. value={serverConfig.username}
  186. onChange={(e) => setServerConfig({ ...serverConfig, username: e.target.value })}
  187. placeholder="root"
  188. 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"
  189. disabled={isConnected}
  190. />
  191. </div>
  192. <div className="space-y-1">
  193. <label className="text-xs font-bold text-slate-500">密码</label>
  194. <input
  195. type="password"
  196. value={serverConfig.password || ''}
  197. onChange={(e) => setServerConfig({ ...serverConfig, password: e.target.value })}
  198. 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"
  199. disabled={isConnected}
  200. />
  201. </div>
  202. </div>
  203. )}
  204. {/* 状态反馈区 */}
  205. {connectionError && (
  206. <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">
  207. <AlertCircle size={18} className="shrink-0 mt-0.5" />
  208. <span className="break-all">{connectionError}</span>
  209. </div>
  210. )}
  211. {isConnected && (
  212. <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">
  213. <CheckCircle2 size={18} />
  214. 已成功连接到服务器
  215. </div>
  216. )}
  217. {/* 操作按钮:移动端垂直排列全宽,桌面端水平 */}
  218. <div className="flex flex-col sm:flex-row gap-3 pt-2">
  219. {!isConnected ? (
  220. <>
  221. <button
  222. onClick={handleConnect}
  223. disabled={connecting}
  224. 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"
  225. >
  226. {connecting ? <RefreshCw size={18} className="animate-spin" /> : <Server size={18} />}
  227. {connecting ? '正在连接...' : '立即连接'}
  228. </button>
  229. <button
  230. onClick={async () => {
  231. try {
  232. const res = await api.get('/api/ping');
  233. alert(`API OK: ${JSON.stringify(res.data)}`);
  234. } catch (err: any) {
  235. alert(`API Error: ${err.message}`);
  236. }
  237. }}
  238. 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"
  239. >
  240. <RefreshCw size={18} /> 测试 API
  241. </button>
  242. </>
  243. ) : (
  244. <button
  245. onClick={handleDisconnect}
  246. 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"
  247. >
  248. <Server size={18} /> 断开连接
  249. </button>
  250. )}
  251. </div>
  252. </div>
  253. </div>
  254. {/* === 卡片 2: 功能控制区 (连接后显示) === */}
  255. {isConnected && (
  256. <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden animate-in fade-in slide-in-from-bottom-4">
  257. {/* Tab 导航:移动端支持横向滚动 */}
  258. <div className="border-b border-slate-100 overflow-x-auto scrollbar-hide">
  259. <nav className="flex min-w-max px-2">
  260. <button
  261. onClick={() => setActiveTab('docker')}
  262. className={`px-4 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${
  263. activeTab === 'docker' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
  264. }`}
  265. >
  266. <Play size={18} /> Docker 控制
  267. </button>
  268. <button
  269. onClick={() => setActiveTab('logs')}
  270. className={`px-4 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${
  271. activeTab === 'logs' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
  272. }`}
  273. >
  274. <FileText size={18} /> 日志查看
  275. </button>
  276. <button
  277. onClick={() => setActiveTab('config')}
  278. className={`px-4 py-4 text-sm font-bold border-b-2 transition-colors flex items-center gap-2 ${
  279. activeTab === 'config' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800'
  280. }`}
  281. >
  282. <Settings size={18} /> 配置文件
  283. </button>
  284. </nav>
  285. </div>
  286. <div className="p-4 md:p-6">
  287. {activeTab === 'docker' && (
  288. <DockerControl
  289. serverConfig={serverConfig}
  290. serverId={usePreConfigured ? selectedServerId : undefined}
  291. />
  292. )}
  293. {activeTab === 'logs' && (
  294. <LogViewer
  295. serverConfig={serverConfig}
  296. serverId={usePreConfigured ? selectedServerId : undefined}
  297. />
  298. )}
  299. {activeTab === 'config' && (
  300. <ConfigManager
  301. serverConfig={serverConfig}
  302. serverId={usePreConfigured ? selectedServerId : undefined}
  303. />
  304. )}
  305. </div>
  306. </div>
  307. )}
  308. </div>
  309. );
  310. }