/** * Docker管理模块 - 专注于 Docker 容器表格的渲染和交互 */ const dockerManager = { // 初始化函数 - 只做基本的 UI 设置或事件监听(如果需要) init: function() { // 减少日志输出 // console.log('[dockerManager] Initializing Docker manager UI components...'); // 可以在这里添加下拉菜单的全局事件监听器等 this.setupActionDropdownListener(); // 立即显示加载状态和表头 this.showLoadingState(); // 添加对Bootstrap下拉菜单的初始化 document.addEventListener('DOMContentLoaded', () => { this.initDropdowns(); }); // 当文档已经加载完成时立即初始化 if (document.readyState === 'complete' || document.readyState === 'interactive') { this.initDropdowns(); } return Promise.resolve(); }, // 初始化Bootstrap下拉菜单组件 initDropdowns: function() { // 减少日志输出 // console.log('[dockerManager] Initializing Bootstrap dropdowns...'); // 直接初始化,不使用setTimeout避免延迟导致的问题 try { // 动态初始化所有下拉菜单 const dropdownElements = document.querySelectorAll('[data-bs-toggle="dropdown"]'); if (dropdownElements.length === 0) { return; // 如果没有找到下拉元素,直接返回 } if (window.bootstrap && window.bootstrap.Dropdown) { dropdownElements.forEach(el => { try { new window.bootstrap.Dropdown(el); } catch (e) { // 静默处理错误,不要输出到控制台 } }); } else { console.warn('Bootstrap Dropdown 组件未找到,将尝试使用jQuery初始化'); // 尝试使用jQuery初始化(如果存在) if (window.jQuery) { window.jQuery('[data-bs-toggle="dropdown"]').dropdown(); } } } catch (error) { // 静默处理错误 } }, // 显示表格加载状态 - 保持,用于初始渲染和刷新 showLoadingState() { const table = document.getElementById('dockerStatusTable'); const tbody = document.getElementById('dockerStatusTableBody'); // 首先创建表格标题区域(如果不存在) let tableContainer = document.getElementById('dockerTableContainer'); if (tableContainer) { // 添加表格标题区域 - 只有不存在时才添加 if (!tableContainer.querySelector('.docker-table-header')) { const tableHeader = document.createElement('div'); tableHeader.className = 'docker-table-header'; tableHeader.innerHTML = `

Docker 容器管理

`; // 插入到表格前面 if (table) { tableContainer.insertBefore(tableHeader, table); // 添加刷新按钮事件 const refreshBtn = document.getElementById('refreshDockerBtn'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { if (window.systemStatus && typeof window.systemStatus.refreshSystemStatus === 'function') { window.systemStatus.refreshSystemStatus(); } }); } } } } if (table && tbody) { // 添加Excel风格表格类 table.classList.add('excel-table'); // 确保表头存在并正确渲染 const thead = table.querySelector('thead'); if (thead) { thead.innerHTML = ` 容器ID 容器名称 镜像名称 运行状态 操作 `; } // 显示加载状态 tbody.innerHTML = `

正在加载容器列表...

`; } }, // 渲染容器表格 - 核心渲染函数,由 systemStatus 调用 renderContainersTable(containers, dockerStatus) { // 减少详细日志输出 // console.log(`[dockerManager] Rendering containers table. Containers count: ${containers ? containers.length : 0}`); const tbody = document.getElementById('dockerStatusTableBody'); if (!tbody) { return; } // 确保表头存在 (showLoadingState 应该已经创建) const table = document.getElementById('dockerStatusTable'); if (table) { const thead = table.querySelector('thead'); if (!thead || !thead.querySelector('tr')) { // 重新创建表头 const newThead = thead || document.createElement('thead'); newThead.innerHTML = ` 容器ID 容器名称 镜像名称 运行状态 操作 `; if (!thead) { table.insertBefore(newThead, tbody); } } } // 1. 检查 Docker 服务状态 if (dockerStatus !== 'running') { tbody.innerHTML = ` Docker 服务未运行 `; return; } // 2. 检查容器数组是否有效且有内容 if (!Array.isArray(containers) || containers.length === 0) { tbody.innerHTML = ` 暂无运行中的Docker容器 `; return; } // 3. 渲染容器列表 let html = ''; containers.forEach(container => { const status = container.State || container.status || '未知'; const statusClass = this.getContainerStatusClass(status); const containerId = container.Id || container.id || '未知'; const containerName = container.Names?.[0]?.substring(1) || container.name || '未知'; const containerImage = container.Image || container.image || '未知'; // 添加lowerStatus变量定义,修复错误 const lowerStatus = status.toLowerCase(); // 替换下拉菜单实现为直接的操作按钮 let actionButtons = ''; // 基本操作:查看日志和详情 actionButtons += ` `; // 根据状态显示不同操作 if (lowerStatus.includes('running')) { actionButtons += ` `; } else if (lowerStatus.includes('exited') || lowerStatus.includes('stopped') || lowerStatus.includes('created')) { actionButtons += ` `; } else if (lowerStatus.includes('paused')) { actionButtons += ` `; } // 更新容器按钮(总是显示) actionButtons += ` `; html += ` ${containerId.substring(0, 12)} ${containerName} ${containerImage} ${status}
${actionButtons}
`; }); tbody.innerHTML = html; // 为所有操作按钮绑定事件 this.setupButtonListeners(); }, // 为所有操作按钮绑定事件 setupButtonListeners() { // 查找所有操作按钮并绑定点击事件 document.querySelectorAll('.action-cell button').forEach(button => { const action = Array.from(button.classList).find(cls => cls.startsWith('action-')); if (!action) return; const containerId = button.dataset.id; if (!containerId) return; button.addEventListener('click', (event) => { event.preventDefault(); const containerName = button.dataset.name; const containerImage = button.dataset.image; switch (action) { case 'action-logs': this.showContainerLogs(containerId, containerName); break; case 'action-details': this.showContainerDetails(containerId); break; case 'action-stop': this.stopContainer(containerId); break; case 'action-start': this.startContainer(containerId); break; case 'action-restart': this.restartContainer(containerId); break; case 'action-remove': this.removeContainer(containerId); break; case 'action-unpause': // this.unpauseContainer(containerId); // 假设有这个函数 console.warn('Unpause action not implemented yet.'); break; case 'action-update': this.updateContainer(containerId, containerImage); break; default: console.warn('Unknown action:', action); } }); }); }, // 获取容器状态对应的 CSS 类 - 保持 getContainerStatusClass(state) { if (!state) return 'status-unknown'; state = state.toLowerCase(); if (state.includes('running')) return 'status-running'; if (state.includes('created')) return 'status-created'; if (state.includes('exited') || state.includes('stopped')) return 'status-stopped'; if (state.includes('paused')) return 'status-paused'; return 'status-unknown'; }, // 设置下拉菜单动作的事件监听 (委托方法 - 现在直接使用按钮,不再需要) setupActionDropdownListener() { // 这个方法留作兼容性,但实际上我们现在直接使用按钮而非下拉菜单 }, // 查看日志 (示例:用 SweetAlert 显示) async showContainerLogs(containerId, containerName) { core.showLoading('正在加载日志...'); try { // 注意: 后端 /api/docker/containers/:id/logs 需要存在并返回日志文本 const response = await fetch(`/api/docker/containers/${containerId}/logs`); if (!response.ok) { const errorData = await response.json().catch(() => ({ details: '无法解析错误响应' })); throw new Error(errorData.details || `获取日志失败 (${response.status})`); } const logs = await response.text(); core.hideLoading(); Swal.fire({ title: `容器日志: ${containerName || containerId.substring(0, 6)}`, html: `
${logs.replace(//g, ">")}
`, width: '80%', customClass: { htmlContainer: 'swal2-logs-container', popup: 'swal2-logs-popup' }, confirmButtonText: '关闭' }); } catch (error) { core.hideLoading(); core.showAlert(`查看日志失败: ${error.message}`, 'error'); logger.error(`[dockerManager] Error fetching logs for ${containerId}:`, error); } }, // 显示容器详情 (示例:用 SweetAlert 显示) async showContainerDetails(containerId) { core.showLoading('正在加载详情...'); try { // 注意: 后端 /api/docker/containers/:id 需要存在并返回详细信息 const response = await fetch(`/api/docker/containers/${containerId}`); if (!response.ok) { const errorData = await response.json().catch(() => ({ details: '无法解析错误响应' })); throw new Error(errorData.details || `获取详情失败 (${response.status})`); } const details = await response.json(); core.hideLoading(); // 格式化显示详情 let detailsHtml = '
'; for (const key in details) { detailsHtml += `

${key}: ${JSON.stringify(details[key], null, 2)}

`; } detailsHtml += '
'; Swal.fire({ title: `容器详情: ${details.Name || containerId.substring(0, 6)}`, html: detailsHtml, width: '80%', confirmButtonText: '关闭' }); } catch (error) { core.hideLoading(); core.showAlert(`查看详情失败: ${error.message}`, 'error'); logger.error(`[dockerManager] Error fetching details for ${containerId}:`, error); } }, // 启动容器 async startContainer(containerId) { core.showLoading('正在启动容器...'); try { const response = await fetch(`/api/docker/containers/${containerId}/start`, { method: 'POST' }); const data = await response.json(); core.hideLoading(); if (!response.ok) throw new Error(data.details || '启动容器失败'); core.showAlert('容器启动成功', 'success'); systemStatus.refreshSystemStatus(); // 刷新整体状态 } catch (error) { core.hideLoading(); core.showAlert(`启动容器失败: ${error.message}`, 'error'); logger.error(`[dockerManager] Error starting container ${containerId}:`, error); } }, // 停止容器 async stopContainer(containerId) { core.showLoading('正在停止容器...'); try { const response = await fetch(`/api/docker/containers/${containerId}/stop`, { method: 'POST' }); const data = await response.json(); core.hideLoading(); if (!response.ok && response.status !== 304) { // 304 Not Modified 也算成功(已停止) throw new Error(data.details || '停止容器失败'); } core.showAlert(data.message || '容器停止成功', 'success'); systemStatus.refreshSystemStatus(); // 刷新整体状态 } catch (error) { core.hideLoading(); core.showAlert(`停止容器失败: ${error.message}`, 'error'); logger.error(`[dockerManager] Error stopping container ${containerId}:`, error); } }, // 重启容器 async restartContainer(containerId) { core.showLoading('正在重启容器...'); try { const response = await fetch(`/api/docker/containers/${containerId}/restart`, { method: 'POST' }); const data = await response.json(); core.hideLoading(); if (!response.ok) throw new Error(data.details || '重启容器失败'); core.showAlert('容器重启成功', 'success'); systemStatus.refreshSystemStatus(); // 刷新整体状态 } catch (error) { core.hideLoading(); core.showAlert(`重启容器失败: ${error.message}`, 'error'); logger.error(`[dockerManager] Error restarting container ${containerId}:`, error); } }, // 删除容器 (带确认) removeContainer(containerId) { Swal.fire({ title: '确认删除?', text: `确定要删除容器 ${containerId.substring(0, 6)} 吗?此操作不可恢复!`, icon: 'warning', showCancelButton: true, confirmButtonColor: 'var(--danger-color)', cancelButtonColor: '#6c757d', confirmButtonText: '确认删除', cancelButtonText: '取消' }).then(async (result) => { if (result.isConfirmed) { core.showLoading('正在删除容器...'); try { const response = await fetch(`/api/docker/containers/${containerId}/remove`, { method: 'POST' }); // 使用 remove const data = await response.json(); core.hideLoading(); if (!response.ok) throw new Error(data.details || '删除容器失败'); core.showAlert(data.message || '容器删除成功', 'success'); systemStatus.refreshSystemStatus(); // 刷新整体状态 } catch (error) { core.hideLoading(); core.showAlert(`删除容器失败: ${error.message}`, 'error'); logger.error(`[dockerManager] Error removing container ${containerId}:`, error); } } }); }, // --- 新增:更新容器函数 --- async updateContainer(containerId, currentImage) { const imageName = currentImage.split(':')[0]; // 提取基础镜像名 const { value: newTag } = await Swal.fire({ title: `更新容器: ${imageName}`, input: 'text', inputLabel: '请输入新的镜像标签 (例如 latest, v1.2)', inputValue: 'latest', // 默认值 showCancelButton: true, confirmButtonText: '开始更新', cancelButtonText: '取消', confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', inputValidator: (value) => { if (!value || value.trim() === '') { return '镜像标签不能为空!'; } }, // 美化弹窗样式 customClass: { container: 'update-container', popup: 'update-popup', header: 'update-header', title: 'update-title', closeButton: 'update-close', icon: 'update-icon', image: 'update-image', content: 'update-content', input: 'update-input', actions: 'update-actions', confirmButton: 'update-confirm', cancelButton: 'update-cancel', footer: 'update-footer' } }); if (newTag) { // 显示进度弹窗 Swal.fire({ title: '更新容器', html: `

正在更新容器 ${containerId.substring(0, 8)}

镜像: ${imageName}:${newTag.trim()}

准备中...
`, showConfirmButton: false, allowOutsideClick: false, allowEscapeKey: false, didOpen: () => { const progressBar = Swal.getPopup().querySelector('.progress-bar'); const progressStatus = Swal.getPopup().querySelector('.progress-status'); // 设置初始进度 progressBar.style.width = '0%'; progressBar.style.backgroundColor = '#4CAF50'; // 模拟进度动画 let progress = 0; const progressInterval = setInterval(() => { // 进度最多到95%,剩下的在请求完成后处理 if (progress < 95) { progress += Math.random() * 3; if (progress > 95) progress = 95; progressBar.style.width = `${progress}%`; // 更新状态文本 if (progress < 30) { progressStatus.textContent = "拉取新镜像..."; } else if (progress < 60) { progressStatus.textContent = "准备更新容器..."; } else if (progress < 90) { progressStatus.textContent = "应用新配置..."; } else { progressStatus.textContent = "即将完成..."; } } }, 300); // 发送更新请求 this.performContainerUpdate(containerId, newTag.trim(), progressBar, progressStatus, progressInterval); } }); } }, // 执行容器更新请求 async performContainerUpdate(containerId, newTag, progressBar, progressStatus, progressInterval) { try { const response = await fetch(`/api/docker/containers/${containerId}/update`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tag: newTag }) }); // 清除进度定时器 clearInterval(progressInterval); if (response.ok) { const data = await response.json(); // 设置进度为100% progressBar.style.width = '100%'; progressStatus.textContent = "更新完成!"; // 显示成功消息 setTimeout(() => { Swal.fire({ icon: 'success', title: '更新成功!', text: data.message || '容器已成功更新', confirmButtonText: '确定' }); // 刷新容器列表 systemStatus.refreshSystemStatus(); }, 800); } else { const data = await response.json().catch(() => ({ error: '解析响应失败', details: '服务器返回了无效的数据' })); // 设置进度条为错误状态 progressBar.style.width = '100%'; progressBar.style.backgroundColor = '#f44336'; progressStatus.textContent = "更新失败"; // 显示错误消息 setTimeout(() => { Swal.fire({ icon: 'error', title: '更新失败', text: data.details || data.error || '未知错误', confirmButtonText: '确定' }); }, 800); } } catch (error) { // 清除进度定时器 clearInterval(progressInterval); // 设置进度条为错误状态 progressBar.style.width = '100%'; progressBar.style.backgroundColor = '#f44336'; progressStatus.textContent = "更新出错"; // 显示错误信息 setTimeout(() => { Swal.fire({ icon: 'error', title: '更新失败', text: error.message || '网络请求失败', confirmButtonText: '确定' }); }, 800); // 记录错误日志 logger.error(`[dockerManager] Error updating container ${containerId} to tag ${newTag}:`, error); } }, // --- 新增:绑定排查按钮事件 --- bindTroubleshootButton() { // 使用 setTimeout 确保按钮已经渲染到 DOM 中 setTimeout(() => { const troubleshootBtn = document.getElementById('docker-troubleshoot-btn'); if (troubleshootBtn) { // 先移除旧监听器,防止重复绑定 troubleshootBtn.replaceWith(troubleshootBtn.cloneNode(true)); const newBtn = document.getElementById('docker-troubleshoot-btn'); // 重新获取克隆后的按钮 if(newBtn) { newBtn.addEventListener('click', () => { if (window.systemStatus && typeof window.systemStatus.showDockerHelp === 'function') { window.systemStatus.showDockerHelp(); } else { console.error('[dockerManager] systemStatus.showDockerHelp is not available.'); // 可以提供一个备用提示 alert('无法显示帮助信息,请检查控制台。'); } }); console.log('[dockerManager] Troubleshoot button event listener bound.'); } else { console.warn('[dockerManager] Cloned troubleshoot button not found after replace.'); } } else { console.warn('[dockerManager] Troubleshoot button not found for binding.'); } }, 0); // 延迟 0ms 执行,让浏览器有机会渲染 } }; // 确保在 DOM 加载后初始化 document.addEventListener('DOMContentLoaded', () => { // 注意:init 现在只设置监听器,不加载数据 // dockerManager.init(); // 可以在 app.js 或 systemStatus.js 初始化完成后调用 });