Jelajahi Sumber

Merge branch 'master' of http://139.224.214.174:3000/jerry/web-ui

jerry 3 bulan lalu
induk
melakukan
00247b4661

+ 1 - 1
.env.local

@@ -1,4 +1,4 @@
 # .env.local
 # 如果你的后端在本地运行,通常是 http://127.0.0.1:8000
 # 如果已经部署到服务器,填写服务器地址,如 https://api.visafly.com
-NEXT_PUBLIC_API_URL=https://visafly.top
+NEXT_PUBLIC_API_URL=https://visafly.top

+ 9 - 7
src/components/admin/remote-server/ConfigManager.tsx

@@ -18,10 +18,12 @@ interface ServerConfig {
 
 export default function ConfigManager({ 
   serverConfig,
-  serverId 
+  serverId,
+  projectPath
 }: { 
   serverConfig: ServerConfig;
   serverId?: string;
+  projectPath?: string;
 }) {
   const [configFile, setConfigFile] = useState<string>('config/troov_config.json');
   const [configData, setConfigData] = useState<any>(null);
@@ -66,8 +68,8 @@ export default function ConfigManager({
     try {
       const url = serverId ? '/api/remote/server/config/read' : '/api/remote/config/read';
       const payload = serverId 
-        ? { server_id: serverId, config_file: configFile }
-        : { ...serverConfig, config_file: configFile };
+        ? { server_id: serverId, config_file: configFile, project_path: projectPath }
+        : { ...serverConfig, config_file: configFile, project_path: projectPath };
 
       const response = await api.post(url, payload);
       if (response.data.code === 0) {
@@ -107,14 +109,14 @@ export default function ConfigManager({
           if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
              const url = serverId ? '/api/remote/server/config/update' : '/api/remote/config/update';
              const payload = serverId 
-              ? { server_id: serverId, config_file: configFile, key_path: fullPath, value: value }
-              : { ...serverConfig, config_file: configFile, key_path: fullPath, value: value };
+              ? { server_id: serverId, config_file: configFile, key_path: fullPath, value: value, project_path: projectPath }
+              : { ...serverConfig, config_file: configFile, key_path: fullPath, value: value, project_path: projectPath };
              updatePromises.push(api.post(url, payload));
           } else {
             const url = serverId ? '/api/remote/server/config/update' : '/api/remote/config/update';
             const payload = serverId 
-              ? { server_id: serverId, config_file: configFile, key_path: fullPath, value: value }
-              : { ...serverConfig, config_file: configFile, key_path: fullPath, value: value };
+              ? { server_id: serverId, config_file: configFile, key_path: fullPath, value: value, project_path: projectPath }
+              : { ...serverConfig, config_file: configFile, key_path: fullPath, value: value, project_path: projectPath };
             updatePromises.push(api.post(url, payload));
           }
         }

+ 8 - 77
src/components/admin/remote-server/DockerControl.tsx

@@ -21,24 +21,23 @@ interface ContainerStatus {
 
 export default function DockerControl({ 
   serverConfig, 
-  serverId 
+  serverId,
+  projectPath
 }: { 
   serverConfig: ServerConfig;
   serverId?: string;
+  projectPath?: string;
 }) {
   const [containers, setContainers] = useState<Record<string, ContainerStatus>>({});
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
-  const [selectedContainer, setSelectedContainer] = useState<string | null>(null);
-  const [logs, setLogs] = useState<string>('');
-  const [logsLines, setLogsLines] = useState(100);
 
   const fetchStatus = async () => {
     setLoading(true);
     setError(null);
     try {
       const url = serverId ? '/api/remote/server/docker/status' : '/api/remote/docker/status';
-      const payload = serverId ? { server_id: serverId } : serverConfig;
+      const payload = serverId ? { server_id: serverId, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
       const response = await api.post(url, payload);
       if (response.data.code === 0) {
         setContainers(response.data.data.containers || {});
@@ -54,7 +53,7 @@ export default function DockerControl({
 
   useEffect(() => {
     fetchStatus();
-  }, [serverId]);
+  }, [serverId, projectPath]);
 
   const handleDockerAction = async (action: 'start' | 'stop' | 'restart', containerName: string) => {
     setLoading(true);
@@ -62,8 +61,8 @@ export default function DockerControl({
     try {
       const url = serverId ? `/api/remote/server/docker/${action}` : `/api/remote/docker/${action}`;
       const payload = serverId 
-        ? { server_id: serverId, container_name: containerName }
-        : { ...serverConfig, container_name: containerName };
+        ? { server_id: serverId, container_name: containerName, project_path: projectPath }
+        : { ...serverConfig, container_name: containerName, project_path: projectPath };
         
       const response = await api.post(url, payload);
       if (response.data.code === 0) {
@@ -84,7 +83,7 @@ export default function DockerControl({
     setError(null);
     try {
       const url = serverId ? `/api/remote/server/docker/${action}` : `/api/remote/docker/${action}`;
-      const payload = serverId ? { server_id: serverId } : serverConfig;
+      const payload = serverId ? { server_id: serverId, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
       const response = await api.post(url, payload);
       if (response.data.code === 0) {
         alert('操作成功');
@@ -99,29 +98,6 @@ export default function DockerControl({
     }
   };
 
-  const fetchLogs = async (containerName: string) => {
-    setLoading(true);
-    setError(null);
-    try {
-      const url = serverId ? '/api/remote/server/docker/logs' : '/api/remote/docker/logs';
-      const payload = serverId 
-        ? { server_id: serverId, container_name: containerName, lines: logsLines, follow: false }
-        : { ...serverConfig, container_name: containerName, lines: logsLines, follow: false };
-
-      const response = await api.post(url, payload);
-      if (response.data.code === 0) {
-        setLogs(response.data.data.logs || '');
-        setSelectedContainer(containerName);
-      } else {
-        setError(response.data.message || '获取日志失败');
-      }
-    } catch (err: any) {
-      setError(err.response?.data?.message || err.message || '获取日志失败');
-    } finally {
-      setLoading(false);
-    }
-  };
-
   return (
     <div className="space-y-6">
       {/* 操作栏 */}
@@ -226,14 +202,6 @@ export default function DockerControl({
                         >
                           <RotateCw size={16} />
                         </button>
-                        <button
-                          onClick={() => fetchLogs(name)}
-                          disabled={loading}
-                          className="p-1.5 text-slate-600 hover:bg-slate-100 rounded transition-colors disabled:opacity-50"
-                          title="查看日志"
-                        >
-                          <FileText size={16} />
-                        </button>
                       </div>
                     </td>
                   </tr>
@@ -295,49 +263,12 @@ export default function DockerControl({
                       <span className="text-[10px]">重启</span>
                     </button>
                   </div>
-                  <button
-                    onClick={() => fetchLogs(name)}
-                    disabled={loading}
-                    className="flex flex-col items-center gap-1 text-slate-600 disabled:opacity-30"
-                  >
-                    <FileText size={18} />
-                    <span className="text-[10px]">日志</span>
-                  </button>
                 </div>
               </div>
             ))
           )}
         </div>
       </div>
-
-      {/* 日志查看 */}
-      {selectedContainer && (
-        <div className="bg-slate-900 rounded-lg border border-slate-200 p-4">
-          <div className="flex items-center justify-between mb-3">
-            <h4 className="text-sm font-semibold text-white">
-              容器日志: {selectedContainer}
-            </h4>
-            <div className="flex items-center gap-2">
-              <input
-                type="number"
-                value={logsLines}
-                onChange={(e) => setLogsLines(parseInt(e.target.value) || 100)}
-                className="w-20 px-2 py-1 bg-slate-800 text-white text-sm rounded border border-slate-700"
-                placeholder="行数"
-              />
-              <button
-                onClick={() => fetchLogs(selectedContainer)}
-                className="px-3 py-1 bg-blue-600 text-white text-sm rounded hover:bg-blue-700"
-              >
-                刷新
-              </button>
-            </div>
-          </div>
-          <pre className="text-xs text-green-400 font-mono overflow-x-auto max-h-96 overflow-y-auto whitespace-pre-wrap">
-            {logs || '暂无日志'}
-          </pre>
-        </div>
-      )}
     </div>
   );
 }

+ 8 - 4
src/components/admin/remote-server/LogViewer.tsx

@@ -15,10 +15,12 @@ interface ServerConfig {
 
 export default function LogViewer({ 
   serverConfig,
-  serverId 
+  serverId,
+  projectPath
 }: { 
   serverConfig: ServerConfig;
   serverId?: string;
+  projectPath?: string;
 }) {
   const [logFiles, setLogFiles] = useState<string[]>([]);
   const [selectedLog, setSelectedLog] = useState<string>('');
@@ -34,7 +36,7 @@ export default function LogViewer({
     setError(null);
     try {
       const url = serverId ? '/api/remote/server/log/list' : '/api/remote/log/list';
-      const payload = serverId ? { server_id: serverId } : serverConfig;
+      const payload = serverId ? { server_id: serverId, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
       const response = await api.post(url, payload);
       if (response.data.code === 0) {
         setLogFiles(response.data.data.log_files || []);
@@ -61,7 +63,8 @@ export default function LogViewer({
             log_file: selectedLog,
             lines: full ? 100 : lines,
             from_head: fromHead,
-            full: full
+            full: full,
+            project_path: projectPath
           }
         : {
             ...serverConfig,
@@ -69,6 +72,7 @@ export default function LogViewer({
             lines: full ? 100 : lines,
             from_head: fromHead,
             full: full,
+            project_path: projectPath,
           };
 
       const response = await api.post(url, payload);
@@ -86,7 +90,7 @@ export default function LogViewer({
 
   useEffect(() => {
     fetchLogList();
-  }, [serverId]);
+  }, [serverId, projectPath]);
 
   useEffect(() => {
     if (selectedLog) {

+ 73 - 39
src/components/admin/remote-server/RemoteServerControl.tsx

@@ -27,6 +27,7 @@ export default function RemoteServerControl() {
   const [usePreConfigured, setUsePreConfigured] = useState(true);
   const [preConfiguredServers, setPreConfiguredServers] = useState<PreConfiguredServer[]>([]);
   const [selectedServerId, setSelectedServerId] = useState<string>('');
+  const [selectedPath, setSelectedPath] = useState<string>('/root/troov-asyncio');
   const [serverConfig, setServerConfig] = useState<ServerConfig>({
     host: '',
     port: 22,
@@ -66,14 +67,20 @@ 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,
+          project_path: selectedPath
+        });
       } else {
         if (!serverConfig.host || !serverConfig.username) {
           setConnectionError('请填写服务器地址和用户名');
           setConnecting(false);
           return;
         }
-        response = await api.post('/api/remote/docker/status', serverConfig);
+        response = await api.post('/api/remote/docker/status', {
+          ...serverConfig,
+          project_path: selectedPath
+        });
       }
       
       if (response.data.code === 0) {
@@ -144,53 +151,77 @@ export default function RemoteServerControl() {
         
         {/* 卡片内容 */}
         <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="grid grid-cols-1 md:grid-cols-2 gap-4">
+            {usePreConfigured ? (
+              <div className="space-y-1">
+                <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="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="block text-xs font-bold text-slate-500 uppercase mb-2">项目路径</label>
               <div className="relative">
                 <select
-                  value={selectedServerId}
-                  onChange={(e) => setSelectedServerId(e.target.value)}
+                  value={selectedPath}
+                  onChange={(e) => setSelectedPath(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>
-                  ))}
+                  <option value="/root/troov-asyncio">默认项目 (/root/troov-asyncio)</option>
+                  <option value="/root/germany_visa">德国签证 (/root/germany_visa)</option>
                 </select>
                 <div className="absolute right-3 top-3.5 text-slate-400 pointer-events-none">
-                  <Search size={16} />
+                  <Settings 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>
+
+          {!usePreConfigured && (
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2 border-t border-slate-50">
               <div className="space-y-1">
                 <label className="text-xs font-bold text-slate-500">用户名</label>
                 <input
@@ -306,18 +337,21 @@ export default function RemoteServerControl() {
               <DockerControl 
                 serverConfig={serverConfig} 
                 serverId={usePreConfigured ? selectedServerId : undefined} 
+                projectPath={selectedPath}
               />
             )}
             {activeTab === 'logs' && (
               <LogViewer 
                 serverConfig={serverConfig} 
                 serverId={usePreConfigured ? selectedServerId : undefined} 
+                projectPath={selectedPath}
               />
             )}
             {activeTab === 'config' && (
               <ConfigManager 
                 serverConfig={serverConfig} 
                 serverId={usePreConfigured ? selectedServerId : undefined} 
+                projectPath={selectedPath}
               />
             )}
           </div>