| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681 |
- /**
- * 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 = `
- <h2 class="docker-table-title">Docker 容器管理</h2>
- <div class="docker-table-actions">
- <button id="refreshDockerBtn" class="btn btn-sm btn-primary">
- <i class="fas fa-sync-alt me-1"></i> 刷新列表
- </button>
- </div>
- `;
-
- // 插入到表格前面
- 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 = `
- <tr>
- <th style="width: 120px;">容器ID</th>
- <th style="width: 25%;">容器名称</th>
- <th style="width: 35%;">镜像名称</th>
- <th style="width: 100px;">运行状态</th>
- <th style="width: 150px;">操作</th>
- </tr>
- `;
- }
-
- // 显示加载状态
- tbody.innerHTML = `
- <tr class="loading-container">
- <td colspan="5">
- <div class="loading-animation">
- <div class="spinner"></div>
- <p>正在加载容器列表...</p>
- </div>
- </td>
- </tr>
- `;
- }
- },
- // 渲染容器表格 - 核心渲染函数,由 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 = `
- <tr>
- <th style="width: 120px;">容器ID</th>
- <th style="width: 25%;">容器名称</th>
- <th style="width: 35%;">镜像名称</th>
- <th style="width: 100px;">运行状态</th>
- <th style="width: 150px;">操作</th>
- </tr>
- `;
-
- if (!thead) {
- table.insertBefore(newThead, tbody);
- }
- }
- }
- // 1. 检查 Docker 服务状态
- if (dockerStatus !== 'running') {
- tbody.innerHTML = `
- <tr>
- <td colspan="5" class="text-center text-muted py-4">
- <i class="fab fa-docker fa-lg me-2"></i> Docker 服务未运行
- </td>
- </tr>
- `;
- return;
- }
- // 2. 检查容器数组是否有效且有内容
- if (!Array.isArray(containers) || containers.length === 0) {
- tbody.innerHTML = `
- <tr>
- <td colspan="5" class="text-center text-muted py-4">
- <i class="fas fa-info-circle me-2"></i> 暂无运行中的Docker容器
- </td>
- </tr>
- `;
- 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 += `
- <button class="btn btn-sm btn-outline-info mb-1 mr-1 action-logs" data-id="${containerId}" data-name="${containerName}">
- <i class="fas fa-file-alt"></i> 日志
- </button>
- <button class="btn btn-sm btn-outline-secondary mb-1 mr-1 action-details" data-id="${containerId}">
- <i class="fas fa-info-circle"></i> 详情
- </button>
- `;
-
- // 根据状态显示不同操作
- if (lowerStatus.includes('running')) {
- actionButtons += `
- <button class="btn btn-sm btn-outline-warning mb-1 mr-1 action-stop" data-id="${containerId}">
- <i class="fas fa-stop"></i> 停止
- </button>
- <button class="btn btn-sm btn-outline-primary mb-1 mr-1 action-restart" data-id="${containerId}">
- <i class="fas fa-sync-alt"></i> 重启
- </button>
- `;
- } else if (lowerStatus.includes('exited') || lowerStatus.includes('stopped') || lowerStatus.includes('created')) {
- actionButtons += `
- <button class="btn btn-sm btn-outline-success mb-1 mr-1 action-start" data-id="${containerId}">
- <i class="fas fa-play"></i> 启动
- </button>
- <button class="btn btn-sm btn-outline-danger mb-1 mr-1 action-remove" data-id="${containerId}">
- <i class="fas fa-trash-alt"></i> 删除
- </button>
- `;
- } else if (lowerStatus.includes('paused')) {
- actionButtons += `
- <button class="btn btn-sm btn-outline-success mb-1 mr-1 action-unpause" data-id="${containerId}">
- <i class="fas fa-play"></i> 恢复
- </button>
- `;
- }
-
- // 更新容器按钮(总是显示)
- actionButtons += `
- <button class="btn btn-sm btn-outline-primary mb-1 mr-1 action-update" data-id="${containerId}" data-image="${containerImage || ''}">
- <i class="fas fa-cloud-download-alt"></i> 更新
- </button>
- `;
- html += `
- <tr>
- <td data-label="ID" title="${containerId}">${containerId.substring(0, 12)}</td>
- <td data-label="名称" title="${containerName}">${containerName}</td>
- <td data-label="镜像" title="${containerImage}">${containerImage}</td>
- <td data-label="状态"><span class="badge ${statusClass}">${status}</span></td>
- <td data-label="操作" class="action-cell">
- <div class="action-buttons">
- ${actionButtons}
- </div>
- </td>
- </tr>
- `;
- });
-
- 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: `<pre class="container-logs">${logs.replace(/</g, "<").replace(/>/g, ">")}</pre>`,
- 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 = '<div class="container-details">';
- for (const key in details) {
- detailsHtml += `<p><strong>${key}:</strong> ${JSON.stringify(details[key], null, 2)}</p>`;
- }
- detailsHtml += '</div>';
-
- 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: `
- <div class="update-progress">
- <p>正在更新容器 <strong>${containerId.substring(0, 8)}</strong></p>
- <p>镜像: <strong>${imageName}:${newTag.trim()}</strong></p>
- <div class="progress-status">准备中...</div>
- <div class="progress-container">
- <div class="progress-bar"></div>
- </div>
- </div>
- `,
- 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 初始化完成后调用
- });
|