浏览代码

feat: Add container management page operations to implement container restart, stop, and other operations.

dqzboy 1 年之前
父节点
当前提交
55c52ba16d
共有 6 个文件被更改,包括 291 次插入7 次删除
  1. 9 0
      README.md
  2. 6 0
      hubcmdui/README.md
  3. 2 0
      hubcmdui/docker-compose.yaml
  4. 2 0
      hubcmdui/package.json
  5. 111 6
      hubcmdui/server.js
  6. 161 1
      hubcmdui/web/admin.html

+ 9 - 0
README.md

@@ -149,6 +149,7 @@ docker logs -f [容器ID或名称]
 - [x] 支持国内服务器一键部署,解决国内环境无法安装Docker\Compose服务难题
 - [x] 支持主流Linux发行版操作系统,例如Centos、Ubuntu、Rocky、Debian、Rhel等
 - [x] 支持主流ARCH架构下部署,包括linux/amd64、linux/arm64
+- [x] 针对本项目单独开发等Docker Registry管理面板,实现镜像搜索、广告展示、文档教程、容器管理等
 
 ## ✨ 教程
 #### 配置Nginx反向代理
@@ -227,6 +228,14 @@ docker pull gcr.your_domain_name/google-containers/pause:3.1
         <td width="50%" align="center"><img src="https://github.com/dqzboy/Docker-Proxy/assets/42825450/0ddb041b-64f6-4d93-b5bf-85ad3b53d0e0?raw=true"></td>
         <td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c7e368ca-7f1a-4311-9a10-a5f4f06d86d8?raw=true"></td>
     </tr>
+    <tr>
+      <td width="50%" align="center"><b>Docker官方镜像搜索</b></td>
+      <td width="50%" align="center"><b>Docker容器服务管理</b></td>
+    </tr>
+    <tr>
+        <td width="50%" align="center"><img src="https://github.com/user-attachments/assets/8569c5c4-4ce6-4cd4-8547-fa9816019049?raw=true"></td>
+        <td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c90976d2-ed81-4ed6-aff0-e8642bb6c033?raw=true"></td>
+    </tr>
 </table>
 
 ---

+ 6 - 0
hubcmdui/README.md

@@ -127,6 +127,12 @@ docker logs -f [容器ID或名称]
     </tr>
 </table>
 
+<table>
+    <tr>
+        <td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c90976d2-ed81-4ed6-aff0-e8642bb6c033"?raw=true"></td>
+    </tr>
+</table>
+
 ---
 
 ## 🫶 赞助

+ 2 - 0
hubcmdui/docker-compose.yaml

@@ -4,5 +4,7 @@ services:
     container_name: hubcmd-ui
     image: dqzboy/hubcmd-ui:latest
     restart: always
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
     ports:
       - 30080:3000

+ 2 - 0
hubcmdui/package.json

@@ -2,6 +2,8 @@
   "dependencies": {
     "axios": "^1.7.5",
     "bcrypt": "^5.1.1",
+    "cors": "^2.8.5",
+    "dockerode": "^4.0.2",
     "express": "^4.19.2",
     "express-session": "^1.18.0",
     "morgan": "^1.10.0"

+ 111 - 6
hubcmdui/server.js

@@ -7,8 +7,27 @@ const bcrypt = require('bcrypt');
 const crypto = require('crypto');
 const logger = require('morgan'); // 引入 morgan 作为日志工具
 const axios = require('axios'); // 用于发送 HTTP 请求
-
+const Docker = require('dockerode');
 const app = express();
+const cors = require('cors');
+
+let docker = null;
+
+async function initDocker() {
+  if (docker === null) {
+    docker = new Docker();
+    try {
+      await docker.ping();
+      console.log('成功连接到 Docker 守护进程');
+    } catch (err) {
+      console.error('无法连接到 Docker 守护进程:', err);
+      docker = null;
+    }
+  }
+  return docker;
+}
+
+app.use(cors());
 app.use(express.json());
 app.use(express.static('web'));
 app.use(bodyParser.urlencoded({ extended: true }));
@@ -124,7 +143,6 @@ async function readDocumentation() {
   try {
     await ensureDocumentationDir();
     const files = await fs.readdir(DOCUMENTATION_DIR);
-    console.log('Files in documentation directory:', files);  // 添加日志
 
     const documents = await Promise.all(files.map(async file => {
       const filePath = path.join(DOCUMENTATION_DIR, file);
@@ -139,7 +157,6 @@ async function readDocumentation() {
     }));
 
     const publishedDocuments = documents.filter(doc => doc.published);
-    console.log('Published documents:', publishedDocuments);  // 添加日志
     return publishedDocuments;
   } catch (error) {
     console.error('Error reading documentation:', error);
@@ -205,9 +222,11 @@ app.post('/api/change-password', async (req, res) => {
 
 // 需要登录验证的中间件
 function requireLogin(req, res, next) {
+  console.log('Session:', req.session); // 添加这行
   if (req.session.user) {
     next();
   } else {
+    console.log('用户未登录'); // 添加这行
     res.status(401).json({ error: 'Not logged in' });
   }
 }
@@ -310,7 +329,6 @@ app.post('/api/documentation/:id/toggle-publish', requireLogin, async (req, res)
 app.get('/api/documentation', async (req, res) => {
   try {
     const documents = await readDocumentation();
-    console.log('Sending documents:', documents);  // 添加日志
     res.json(documents);
   } catch (error) {
     console.error('Error in /api/documentation:', error);
@@ -379,11 +397,9 @@ app.get('/api/documentation-list', async (req, res) => {
 app.get('/api/documentation/:id', async (req, res) => {
   try {
     const docId = req.params.id;
-    console.log('Fetching document with id:', docId);  // 添加日志
     const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
     const content = await fs.readFile(docPath, 'utf8');
     const doc = JSON.parse(content);
-    console.log('Sending document:', doc);  // 添加日志
     res.json(doc);
   } catch (error) {
     console.error('Error reading document:', error);
@@ -391,6 +407,95 @@ app.get('/api/documentation/:id', async (req, res) => {
   }
 });
 
+
+
+
+// API端点来获取Docker容器状态
+app.get('/api/docker-status', requireLogin, async (req, res) => {
+  try {
+    const docker = await initDocker();
+    if (!docker) {
+      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
+    }
+    const containers = await docker.listContainers({ all: true });
+    const containerStatus = await Promise.all(containers.map(async (container) => {
+      const containerInfo = await docker.getContainer(container.Id).inspect();
+      const stats = await docker.getContainer(container.Id).stats({ stream: false });
+      
+      // 计算 CPU 使用率
+      const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
+      const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
+      const cpuUsage = (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100;
+      
+      // 计算内存使用率
+      const memoryUsage = stats.memory_stats.usage / stats.memory_stats.limit * 100;
+
+      return {
+        id: container.Id.slice(0, 12),
+        name: container.Names[0].replace(/^\//, ''),
+        image: container.Image,
+        state: containerInfo.State.Status,
+        status: container.Status,
+        cpu: cpuUsage.toFixed(2) + '%',
+        memory: memoryUsage.toFixed(2) + '%',
+        created: new Date(container.Created * 1000).toLocaleString()
+      };
+    }));
+    res.json(containerStatus);
+  } catch (error) {
+    console.error('获取 Docker 状态时出错:', error);
+    res.status(500).json({ error: '获取 Docker 状态失败', details: error.message });
+  }
+});
+
+// API端点:重启容器
+app.post('/api/docker/restart/:id', requireLogin, async (req, res) => {
+  try {
+    const docker = await initDocker();
+    if (!docker) {
+      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
+    }
+    const container = docker.getContainer(req.params.id);
+    await container.restart();
+    res.json({ success: true });
+  } catch (error) {
+    console.error('重启容器失败:', error);
+    res.status(500).json({ error: '重启容器失败', details: error.message });
+  }
+});
+
+// API端点:停止容器
+app.post('/api/docker/stop/:id', requireLogin, async (req, res) => {
+  try {
+    const docker = await initDocker();
+    if (!docker) {
+      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
+    }
+    const container = docker.getContainer(req.params.id);
+    await container.stop();
+    res.json({ success: true });
+  } catch (error) {
+    console.error('停止容器失败:', error);
+    res.status(500).json({ error: '停止容器失败', details: error.message });
+  }
+});
+
+// API端点:获取单个容器的状态
+app.get('/api/docker/status/:id', requireLogin, async (req, res) => {
+  try {
+    const docker = await initDocker();
+    if (!docker) {
+      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
+    }
+    const container = docker.getContainer(req.params.id);
+    const containerInfo = await container.inspect();
+    res.json({ state: containerInfo.State.Status });
+  } catch (error) {
+    console.error('获取容器状态失败:', error);
+    res.status(500).json({ error: '获取容器状态失败', details: error.message });
+  }
+});
+
 // 启动服务器
 const PORT = process.env.PORT || 3000;
 app.listen(PORT, () => {

+ 161 - 1
hubcmdui/web/admin.html

@@ -86,6 +86,7 @@
             font-family: inherit;
             font-size: inherit;
         }
+
         .action-btn:hover {
             background-color: #0256b9;
         }
@@ -400,6 +401,21 @@
                 max-width: 100%;
             }
         }
+
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+
+        .loading {
+            display: inline-block;
+            width: 20px;
+            height: 20px;
+            border: 3px solid rgba(0, 0, 0, 0.1);
+            border-radius: 50%;
+            border-top: 3px solid #0366d6;
+            animation: spin 1s linear infinite;
+        }
     </style>
 </head>
 <body>
@@ -415,6 +431,7 @@
                 <li data-section="ad-management">广告管理</li>
                 <li data-section="documentation-management">文档管理</li>            
                 <li data-section="password-change">修改密码</li>
+                <li data-section="docker-status">Docker 服务状态</li>
             </ul>
         </div>
         <div class="content-area">
@@ -490,6 +507,7 @@
                     </div>
                 </div>  
 
+                <!-- 修改密码部分 -->
                 <div id="password-change" class="content-section">
                     <h2 class="menu-label">修改密码</h2>
                     <label for="currentPassword">当前密码</label>
@@ -500,6 +518,28 @@
                     <span id="passwordStrength" style="color: red;"></span>
                     <button type="button" onclick="changePassword()">修改密码</button>
                 </div>
+
+                <!-- Docker服务状态 -->
+                <div id="docker-status" class="content-section">
+                    <h1 class="admin-title">Docker 服务状态</h1>
+                    <table id="dockerStatusTable">
+                      <thead>
+                        <tr>
+                          <th>容器 ID</th>
+                          <th>名称</th>
+                          <th>镜像</th>
+                          <th>状态</th>
+                          <th>CPU</th>
+                          <th>内存</th>
+                          <th>创建时间</th>
+                        </tr>
+                      </thead>
+                      <tbody id="dockerStatusTableBody">
+                        <!-- Docker 容器状态将在这里动态添加 -->
+                      </tbody>
+                    </table>
+                    <button type="button" onclick="refreshDockerStatus()">刷新状态</button>
+                  </div>
             </div>
         </div>
     </div>
@@ -1278,12 +1318,132 @@
             }
         }
 
+        async function loadDockerStatus() {
+            try {
+                const response = await fetch('/api/docker-status');
+                if (!response.ok) {
+                    if (response.status === 503) {
+                        throw new Error('无法连接到 Docker 守护进程');
+                    }
+                    throw new Error('Failed to fetch Docker status');
+                }
+                const containerStatus = await response.json();
+                renderDockerStatus(containerStatus);
+            } catch (error) {
+                console.error('Error loading Docker status:', error);
+                alert('加载 Docker 状态失败: ' + error.message);
+                // 清空状态表格并显示错误信息
+                const tbody = document.getElementById('dockerStatusTableBody');
+                tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: red;">${error.message}</td></tr>`;
+            }
+        }
+
+        function renderDockerStatus(containerStatus) {
+            const tbody = document.getElementById('dockerStatusTableBody');
+            tbody.innerHTML = '';
+            
+            // 添加表头
+            const thead = document.getElementById('dockerStatusTable').getElementsByTagName('thead')[0];
+            thead.innerHTML = `
+                <tr>
+                    <th>容器 ID</th>
+                    <th>名称</th>
+                    <th>镜像</th>
+                    <th>状态</th>
+                    <th>CPU</th>
+                    <th>内存</th>
+                    <th>创建时间</th>
+                    <th>操作</th>
+                </tr>
+            `;
+
+            containerStatus.forEach(container => {
+                const row = `
+                <tr>
+                    <td>${container.id}</td>
+                    <td>${container.name}</td>
+                    <td>${container.image}</td>
+                    <td id="status-${container.id}">${container.state}</td>
+                    <td>${container.cpu}</td>
+                    <td>${container.memory}</td>
+                    <td>${container.created}</td>
+                    <td>
+                        <button onclick="restartContainer('${container.id}')" class="action-btn">重启</button>
+                        <button onclick="stopContainer('${container.id}')" class="action-btn">停止</button>
+                    </td>
+                </tr>
+                `;
+                tbody.innerHTML += row;
+            });
+        }
+
+        async function restartContainer(id) {
+            if (confirm('确定要重启这个容器吗?')) {
+                try {
+                    const statusCell = document.getElementById(`status-${id}`);
+                    statusCell.innerHTML = '<div class="loading"></div>';
+                    
+                    const response = await fetch(`/api/docker/restart/${id}`, { method: 'POST' });
+                    if (response.ok) {
+                        await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒,确保状态已更新
+                        const newStatus = await getContainerStatus(id);
+                        statusCell.textContent = newStatus;
+                    } else {
+                        throw new Error('重启失败');
+                    }
+                } catch (error) {
+                    console.error('重启容器失败:', error);
+                    alert('重启容器失败: ' + error.message);
+                    loadDockerStatus(); // 重新加载所有容器状态
+                }
+            }
+        }
+
+        async function stopContainer(id) {
+            if (confirm('确定要停止这个容器吗?')) {
+                try {
+                    const statusCell = document.getElementById(`status-${id}`);
+                    statusCell.innerHTML = '<div class="loading"></div>';
+                    
+                    const response = await fetch(`/api/docker/stop/${id}`, { method: 'POST' });
+                    if (response.ok) {
+                        await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒,确保状态已更新
+                        const newStatus = await getContainerStatus(id);
+                        statusCell.textContent = newStatus;
+                    } else {
+                        throw new Error('停止失败');
+                    }
+                } catch (error) {
+                    console.error('停止容器失败:', error);
+                    alert('停止容器失败: ' + error.message);
+                    loadDockerStatus(); // 重新加载所有容器状态
+                }
+            }
+        }
+
+        async function getContainerStatus(id) {
+            const response = await fetch(`/api/docker/status/${id}`);
+            if (response.ok) {
+                const data = await response.json();
+                return data.state;
+            } else {
+                throw new Error('获取容器状态失败');
+            }
+        }
+
+        function refreshDockerStatus() {
+            loadDockerStatus();
+        }
+
         document.addEventListener('DOMContentLoaded', function() {
         const sidebarItems = document.querySelectorAll('.sidebar li');
         const contentSections = document.querySelectorAll('.content-section');
         if (isLoggedIn) {
             initEditor();
         }
+        if (isLoggedIn) {
+            loadDockerStatus();
+        }
         function showSection(sectionId) {
             contentSections.forEach(section => {
                 if (section.id === sectionId) {
@@ -1310,4 +1470,4 @@
     });
     </script>
 </body>
-</html>
+</html>