/**
* 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() {
try {
console.log('[dockerManager] 初始化下拉菜单...');
// 动态初始化所有下拉菜单按钮
const dropdownElements = document.querySelectorAll('[data-bs-toggle="dropdown"]');
console.log(`[dockerManager] 找到 ${dropdownElements.length} 个下拉元素`);
if (dropdownElements.length === 0) {
return; // 如果没有找到下拉元素,直接返回
}
// 尝试使用所有可能的Bootstrap初始化方法
if (window.bootstrap && typeof window.bootstrap.Dropdown !== 'undefined') {
console.log('[dockerManager] 使用 Bootstrap 5 初始化下拉菜单');
dropdownElements.forEach(el => {
try {
new window.bootstrap.Dropdown(el);
} catch (e) {
console.error('Bootstrap 5 下拉菜单初始化错误:', e);
}
});
} else if (typeof $ !== 'undefined' && typeof $.fn.dropdown !== 'undefined') {
console.log('[dockerManager] 使用 jQuery Bootstrap 初始化下拉菜单');
$(dropdownElements).dropdown();
} else {
console.warn('[dockerManager] 未找到Bootstrap下拉菜单组件,将使用手动下拉实现');
this.setupManualDropdowns();
}
} catch (error) {
console.error('[dockerManager] 初始化下拉菜单错误:', error);
// 失败时使用备用方案
this.setupManualDropdowns();
}
},
// 手动实现下拉菜单功能(备用方案)
setupManualDropdowns: function() {
console.log('[dockerManager] 设置手动下拉菜单...');
// 为所有下拉菜单按钮添加点击事件
document.querySelectorAll('.btn-group .dropdown-toggle').forEach(button => {
// 移除旧事件监听器
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
// 添加新事件监听器
newButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// 查找关联的下拉菜单
const dropdownMenu = this.nextElementSibling;
if (!dropdownMenu || !dropdownMenu.classList.contains('dropdown-menu')) {
return;
}
// 切换显示/隐藏
const isVisible = dropdownMenu.classList.contains('show');
// 先隐藏所有其他打开的下拉菜单
document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
menu.classList.remove('show');
});
// 切换当前菜单
if (!isVisible) {
dropdownMenu.classList.add('show');
// 计算位置 - 精确计算确保菜单位置更美观
const buttonRect = newButton.getBoundingClientRect();
const tableCell = newButton.closest('td');
const tableCellRect = tableCell ? tableCell.getBoundingClientRect() : buttonRect;
// 设置最小宽度,确保下拉菜单够宽
const minWidth = Math.max(180, buttonRect.width * 1.5);
dropdownMenu.style.minWidth = `${minWidth}px`;
// 设置绝对定位
dropdownMenu.style.position = 'absolute';
// 根据屏幕空间计算最佳位置
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const spaceRight = viewportWidth - buttonRect.right;
const spaceBottom = viewportHeight - buttonRect.bottom;
const spaceAbove = buttonRect.top;
// 先移除所有位置相关的类
dropdownMenu.classList.remove('dropdown-menu-top', 'dropdown-menu-right');
// 设置为右对齐,且显示在按钮上方
dropdownMenu.style.right = '0';
dropdownMenu.style.left = 'auto';
// 计算菜单高度 (假设每个菜单项高度为40px,分隔线10px)
const menuItemCount = dropdownMenu.querySelectorAll('.dropdown-item').length;
const dividerCount = dropdownMenu.querySelectorAll('.dropdown-divider').length;
const estimatedMenuHeight = (menuItemCount * 40) + (dividerCount * 10) + 20; // 加上padding
// 优先显示在按钮上方,如果空间不足则显示在下方
if (spaceAbove >= estimatedMenuHeight && spaceAbove > spaceBottom) {
// 显示在按钮上方
dropdownMenu.style.bottom = `${buttonRect.height + 5}px`; // 5px间距
dropdownMenu.style.top = 'auto';
// 设置动画原点为底部
dropdownMenu.style.transformOrigin = 'bottom right';
// 添加上方显示的类
dropdownMenu.classList.add('dropdown-menu-top');
} else {
// 显示在右侧而不是正下方
if (spaceRight >= minWidth && tableCellRect.width > buttonRect.width + 20) {
// 有足够的右侧空间,显示在按钮右侧
dropdownMenu.style.top = '0';
dropdownMenu.style.left = `${buttonRect.width + 5}px`; // 5px间距
dropdownMenu.style.right = 'auto';
dropdownMenu.style.bottom = 'auto';
dropdownMenu.style.transformOrigin = 'left top';
// 添加右侧显示的类
dropdownMenu.classList.add('dropdown-menu-right');
} else {
// 显示在按钮下方,但尝试右对齐
dropdownMenu.style.top = `${buttonRect.height + 5}px`; // 5px间距
dropdownMenu.style.bottom = 'auto';
// 如果下拉菜单宽度超过右侧可用空间,则左对齐显示
if (minWidth > spaceRight) {
dropdownMenu.style.right = 'auto';
dropdownMenu.style.left = '0';
} else {
// 继续使用右对齐
dropdownMenu.classList.add('dropdown-menu-end');
}
dropdownMenu.style.transformOrigin = 'top right';
}
}
// 清除其他可能影响布局的样式
dropdownMenu.style.margin = '0';
dropdownMenu.style.maxHeight = '85vh';
dropdownMenu.style.overflowY = 'auto';
dropdownMenu.style.zIndex = '1050'; // 确保在表格上方
}
// 点击其他区域关闭下拉菜单
const closeHandler = function(event) {
if (!dropdownMenu.contains(event.target) && !newButton.contains(event.target)) {
dropdownMenu.classList.remove('show');
document.removeEventListener('click', closeHandler);
}
};
// 只在打开菜单时添加全局点击监听
if (!isVisible) {
// 延迟一点添加事件,避免立即触发
setTimeout(() => {
document.addEventListener('click', closeHandler);
}, 10);
}
});
});
},
// 显示表格加载状态 - 保持,用于初始渲染和刷新
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', () => {
// 显示加载状态,提高用户体验
this.showRefreshingState(refreshBtn);
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 = `
|
`;
// 添加表格样式
this.applyTableStyles(table);
}
},
// 新增:显示刷新中状态
showRefreshingState(refreshBtn) {
if (!refreshBtn) return;
// 保存原始按钮内容
const originalContent = refreshBtn.innerHTML;
// 更改为加载状态
refreshBtn.innerHTML = ' 刷新中...';
refreshBtn.disabled = true;
refreshBtn.classList.add('refreshing');
// 添加样式使按钮看起来正在加载
const style = document.createElement('style');
style.textContent = `
.btn.refreshing {
opacity: 0.8;
cursor: not-allowed;
}
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
.btn.refreshing i {
animation: pulse 1.5s infinite;
}
.table-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
border-radius: 0.25rem;
}
.table-overlay .spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
// 检查是否已经添加了样式
const existingStyle = document.querySelector('style[data-for="refresh-button"]');
if (!existingStyle) {
style.setAttribute('data-for', 'refresh-button');
document.head.appendChild(style);
}
// 获取表格和容器
const table = document.getElementById('dockerStatusTable');
const tableContainer = document.getElementById('dockerTableContainer');
// 移除任何现有的覆盖层
const existingOverlay = document.querySelector('.table-overlay');
if (existingOverlay) {
existingOverlay.remove();
}
// 创建一个覆盖层而不是替换表格内容
if (table) {
// 设置表格容器为相对定位,以便正确放置覆盖层
if (tableContainer) {
tableContainer.style.position = 'relative';
} else {
table.parentNode.style.position = 'relative';
}
// 创建覆盖层
const overlay = document.createElement('div');
overlay.className = 'table-overlay';
overlay.innerHTML = `
正在更新容器列表...
`;
// 获取表格的位置并设置覆盖层
const tableRect = table.getBoundingClientRect();
overlay.style.width = `${table.offsetWidth}px`;
overlay.style.height = `${table.offsetHeight}px`;
// 将覆盖层添加到表格容器
if (tableContainer) {
tableContainer.appendChild(overlay);
} else {
table.parentNode.appendChild(overlay);
}
}
// 设置超时,防止永久加载状态
setTimeout(() => {
// 如果按钮仍处于加载状态,恢复为原始状态
if (refreshBtn.classList.contains('refreshing')) {
refreshBtn.innerHTML = originalContent;
refreshBtn.disabled = false;
refreshBtn.classList.remove('refreshing');
// 移除覆盖层
const overlay = document.querySelector('.table-overlay');
if (overlay) {
overlay.remove();
}
}
}, 10000); // 10秒超时
},
// 渲染容器表格 - 核心渲染函数,由 systemStatus 调用
renderContainersTable(containers, dockerStatus) {
// 减少详细日志输出
// console.log(`[dockerManager] Rendering containers table. Containers count: ${containers ? containers.length : 0}`);
// 重置刷新按钮状态
const refreshBtn = document.getElementById('refreshDockerBtn');
if (refreshBtn && refreshBtn.classList.contains('refreshing')) {
refreshBtn.innerHTML = ' 刷新列表';
refreshBtn.disabled = false;
refreshBtn.classList.remove('refreshing');
// 移除覆盖层
const overlay = document.querySelector('.table-overlay');
if (overlay) {
overlay.remove();
}
}
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);
}
}
// 应用表格样式
this.applyTableStyles(table);
}
// 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();
// 创建按钮组,使用标准Bootstrap 5下拉菜单语法
let actionButtons = `
`;
html += `
${containerId.substring(0, 12)} |
${containerName} |
${containerImage} |
${status} |
${actionButtons}
|
`;
});
tbody.innerHTML = html;
// 为所有操作按钮绑定事件
this.setupButtonListeners();
// 确保在内容渲染后立即初始化下拉菜单
setTimeout(() => {
this.initDropdowns();
// 备用方法:直接为下拉菜单按钮添加点击事件
this.setupManualDropdowns();
}, 100); // 增加延迟确保DOM完全渲染
},
// 为所有操作按钮绑定事件 - 简化此方法,专注于直接点击处理
setupButtonListeners() {
// 为下拉框选择事件添加处理逻辑
document.querySelectorAll('.action-cell .simple-dropdown').forEach(select => {
select.addEventListener('change', (event) => {
event.preventDefault();
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption || selectedOption.disabled) return;
const action = Array.from(selectedOption.classList).find(cls => cls.startsWith('action-'));
if (!action) return;
const containerId = selectedOption.getAttribute('data-id');
if (!containerId) return;
const containerName = selectedOption.getAttribute('data-name');
const containerImage = selectedOption.getAttribute('data-image');
console.log('处理容器操作:', action, '容器ID:', containerId);
// 执行对应的容器操作
this.handleContainerAction(action, containerId, containerName, containerImage);
// 重置选择,以便下次可以再次选择相同选项
select.selectedIndex = 0;
});
});
// 让下拉框按钮隐藏,只显示select元素
document.querySelectorAll('.simple-dropdown-toggle').forEach(button => {
button.style.display = 'none';
});
// 样式化select元素
document.querySelectorAll('.simple-dropdown').forEach(select => {
select.style.display = 'block';
select.style.width = '100%';
select.style.padding = '0.375rem 0.75rem';
select.style.fontSize = '0.875rem';
select.style.borderRadius = '0.25rem';
select.style.border = '1px solid #ced4da';
select.style.backgroundColor = '#fff';
});
},
// 处理容器操作的统一方法
handleContainerAction(action, containerId, containerName, containerImage) {
console.log('Handling container action:', action, 'for container:', containerId);
switch (action) {
case 'action-logs':
this.showContainerLogs(containerId, containerName);
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':
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';
},
// 设置下拉菜单动作的事件监听 - 简化为空方法,因为使用原生select不需要
setupActionDropdownListener() {
// 不需要特殊处理,使用原生select元素的change事件
},
// 查看日志
async showContainerLogs(containerId, containerName) {
console.log('正在获取日志,容器ID:', 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');
console.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(); // 刷新整体状态
// 刷新已停止容器列表
if (window.app && typeof window.app.refreshStoppedContainers === 'function') {
window.app.refreshStoppedContainers();
}
} 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',
width: '36em', // 增加弹窗宽度
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'
},
// 添加自定义CSS
didOpen: () => {
// 修复输入框宽度
const inputElement = Swal.getInput();
if (inputElement) {
inputElement.style.maxWidth = '100%';
inputElement.style.width = '100%';
inputElement.style.boxSizing = 'border-box';
inputElement.style.margin = '0';
inputElement.style.padding = '0.5rem';
}
// 修复输入标签宽度
const inputLabel = Swal.getPopup().querySelector('.swal2-input-label');
if (inputLabel) {
inputLabel.style.whiteSpace = 'normal';
inputLabel.style.textAlign = 'left';
inputLabel.style.width = '100%';
inputLabel.style.padding = '0 10px';
inputLabel.style.boxSizing = 'border-box';
inputLabel.style.marginBottom = '0.5rem';
}
// 调整弹窗内容区域
const content = Swal.getPopup().querySelector('.swal2-content');
if (content) {
content.style.padding = '0 1.5rem';
content.style.boxSizing = 'border-box';
content.style.width = '100%';
}
}
});
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 执行,让浏览器有机会渲染
},
// 新增方法: 应用表格样式
applyTableStyles(table) {
if (!table) return;
// 添加基本样式
table.style.width = "100%";
table.style.tableLayout = "auto";
table.style.borderCollapse = "collapse";
// 设置表头样式
const thead = table.querySelector('thead');
if (thead) {
thead.style.backgroundColor = "#f8f9fa";
thead.style.fontWeight = "bold";
const thCells = thead.querySelectorAll('th');
thCells.forEach(th => {
th.style.textAlign = "center";
th.style.padding = "10px 8px";
th.style.verticalAlign = "middle";
});
}
// 添加响应式样式
const style = document.createElement('style');
style.textContent = `
#dockerStatusTable {
width: 100%;
table-layout: auto;
}
#dockerStatusTable th, #dockerStatusTable td {
text-align: center;
vertical-align: middle;
padding: 8px;
}
#dockerStatusTable td.action-cell {
padding: 4px;
}
@media (max-width: 768px) {
#dockerStatusTable {
table-layout: fixed;
}
}
`;
// 检查是否已经添加了样式
const existingStyle = document.querySelector('style[data-for="dockerStatusTable"]');
if (!existingStyle) {
style.setAttribute('data-for', 'dockerStatusTable');
document.head.appendChild(style);
}
}
};
// 确保在 DOM 加载后初始化
document.addEventListener('DOMContentLoaded', () => {
// 注意:init 现在只设置监听器,不加载数据
// dockerManager.init();
// 可以在 app.js 或 systemStatus.js 初始化完成后调用
});