| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- 'use client';
- import { useState, useEffect } from 'react';
- import api from '@/lib/api';
- import { Play, Square, RotateCw, RefreshCw, FileText, AlertCircle } from 'lucide-react';
- interface ServerConfig {
- host: string;
- port: number;
- username: string;
- password?: string;
- key_file?: string;
- project_path: string;
- }
- interface ContainerStatus {
- name: string;
- status: string;
- image: string;
- }
- export default function DockerControl({
- serverConfig,
- 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 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, project_path: projectPath } : { ...serverConfig, project_path: projectPath };
- const response = await api.post(url, payload);
- if (response.data.code === 0) {
- setContainers(response.data.data.containers || {});
- } else {
- setError(response.data.message || '获取状态失败');
- }
- } catch (err: any) {
- setError(err.response?.data?.message || err.message || '请求失败');
- } finally {
- setLoading(false);
- }
- };
- useEffect(() => {
- fetchStatus();
- }, [serverId, projectPath]);
- const handleDockerAction = async (action: 'start' | 'stop' | 'restart', containerName: string) => {
- setLoading(true);
- setError(null);
- try {
- const url = serverId ? `/api/remote/server/docker/${action}` : `/api/remote/docker/${action}`;
- const payload = serverId
- ? { 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) {
- alert('操作成功');
- await fetchStatus();
- } else {
- setError(response.data.message || '操作失败');
- }
- } catch (err: any) {
- setError(err.response?.data?.message || err.message || '操作失败');
- } finally {
- setLoading(false);
- }
- };
- const handleComposeAction = async (action: 'up' | 'down') => {
- setLoading(true);
- setError(null);
- try {
- const url = serverId ? `/api/remote/server/docker/${action}` : `/api/remote/docker/${action}`;
- 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('操作成功');
- await fetchStatus();
- } else {
- setError(response.data.message || '操作失败');
- }
- } catch (err: any) {
- setError(err.response?.data?.message || err.message || '操作失败');
- } finally {
- setLoading(false);
- }
- };
- return (
- <div className="space-y-6">
- {/* 操作栏 */}
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
- <div className="flex items-center gap-2">
- <h3 className="text-lg font-semibold text-slate-800">Docker 容器管理</h3>
- <button
- onClick={fetchStatus}
- disabled={loading}
- className="p-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors disabled:opacity-50"
- title="刷新状态"
- >
- <RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
- </button>
- </div>
- <div className="grid grid-cols-2 sm:flex gap-2">
- <button
- onClick={() => handleComposeAction('up')}
- disabled={loading}
- className="px-3 sm:px-4 py-2 bg-green-600 text-white text-sm sm:text-base rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
- >
- <Play size={18} />
- <span className="whitespace-nowrap">全部启动</span>
- </button>
- <button
- onClick={() => handleComposeAction('down')}
- disabled={loading}
- className="px-3 sm:px-4 py-2 bg-red-600 text-white text-sm sm:text-base rounded-md hover:bg-red-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
- >
- <Square size={18} />
- <span className="whitespace-nowrap">全部停止</span>
- </button>
- </div>
- </div>
- {error && (
- <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md flex items-center gap-2">
- <AlertCircle size={18} />
- {error}
- </div>
- )}
- {/* 容器列表 */}
- <div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
- {/* 桌面端表格视图 */}
- <div className="hidden md:block overflow-x-auto">
- <table className="w-full">
- <thead className="bg-slate-50 border-b border-slate-200">
- <tr>
- <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">容器名称</th>
- <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">状态</th>
- <th className="px-4 py-3 text-left text-sm font-semibold text-slate-700">镜像</th>
- <th className="px-4 py-3 text-right text-sm font-semibold text-slate-700">操作</th>
- </tr>
- </thead>
- <tbody className="divide-y divide-slate-200">
- {Object.keys(containers).length === 0 ? (
- <tr>
- <td colSpan={4} className="px-4 py-8 text-center text-slate-500">
- {loading ? '加载中...' : '暂无容器'}
- </td>
- </tr>
- ) : (
- Object.entries(containers).map(([name, container]) => (
- <tr key={name} className="hover:bg-slate-50">
- <td className="px-4 py-3 text-sm text-slate-800 font-medium">{name}</td>
- <td className="px-4 py-3 text-sm">
- <span
- className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${
- container.status.includes('Up')
- ? 'bg-green-100 text-green-800'
- : 'bg-red-100 text-red-800'
- }`}
- >
- {container.status}
- </span>
- </td>
- <td className="px-4 py-3 text-sm text-slate-600">{container.image}</td>
- <td className="px-4 py-3 text-right">
- <div className="flex items-center justify-end gap-2">
- <button
- onClick={() => handleDockerAction('start', name)}
- disabled={loading || container.status.includes('Up')}
- className="p-1.5 text-green-600 hover:bg-green-50 rounded transition-colors disabled:opacity-50"
- title="启动"
- >
- <Play size={16} />
- </button>
- <button
- onClick={() => handleDockerAction('stop', name)}
- disabled={loading || !container.status.includes('Up')}
- className="p-1.5 text-red-600 hover:bg-red-50 rounded transition-colors disabled:opacity-50"
- title="停止"
- >
- <Square size={16} />
- </button>
- <button
- onClick={() => handleDockerAction('restart', name)}
- disabled={loading}
- className="p-1.5 text-blue-600 hover:bg-blue-50 rounded transition-colors disabled:opacity-50"
- title="重启"
- >
- <RotateCw size={16} />
- </button>
- </div>
- </td>
- </tr>
- ))
- )}
- </tbody>
- </table>
- </div>
- {/* 移动端卡片视图 */}
- <div className="md:hidden divide-y divide-slate-200">
- {Object.keys(containers).length === 0 ? (
- <div className="px-4 py-8 text-center text-slate-500">
- {loading ? '加载中...' : '暂无容器'}
- </div>
- ) : (
- Object.entries(containers).map(([name, container]) => (
- <div key={name} className="p-4 space-y-3">
- <div className="flex items-center justify-between">
- <span className="text-sm font-semibold text-slate-800">{name}</span>
- <span
- className={`inline-flex px-2 py-0.5 rounded-full text-[10px] font-medium ${
- container.status.includes('Up')
- ? 'bg-green-100 text-green-800'
- : 'bg-red-100 text-red-800'
- }`}
- >
- {container.status}
- </span>
- </div>
- <div className="text-xs text-slate-500 truncate">
- <span className="font-medium text-slate-600 mr-1">镜像:</span>
- {container.image}
- </div>
- <div className="flex items-center justify-between pt-2 border-t border-slate-100">
- <div className="flex gap-4">
- <button
- onClick={() => handleDockerAction('start', name)}
- disabled={loading || container.status.includes('Up')}
- className="flex flex-col items-center gap-1 text-green-600 disabled:opacity-30"
- >
- <Play size={18} />
- <span className="text-[10px]">启动</span>
- </button>
- <button
- onClick={() => handleDockerAction('stop', name)}
- disabled={loading || !container.status.includes('Up')}
- className="flex flex-col items-center gap-1 text-red-600 disabled:opacity-30"
- >
- <Square size={18} />
- <span className="text-[10px]">停止</span>
- </button>
- <button
- onClick={() => handleDockerAction('restart', name)}
- disabled={loading}
- className="flex flex-col items-center gap-1 text-blue-600 disabled:opacity-30"
- >
- <RotateCw size={18} />
- <span className="text-[10px]">重启</span>
- </button>
- </div>
- </div>
- </div>
- ))
- )}
- </div>
- </div>
- </div>
- );
- }
|