dockerManager.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. /**
  2. * Docker管理模块 - 专注于 Docker 容器表格的渲染和交互
  3. */
  4. const dockerManager = {
  5. // 初始化函数 - 只做基本的 UI 设置或事件监听(如果需要)
  6. init: function() {
  7. // 减少日志输出
  8. // console.log('[dockerManager] Initializing Docker manager UI components...');
  9. // 可以在这里添加下拉菜单的全局事件监听器等
  10. this.setupActionDropdownListener();
  11. // 立即显示加载状态和表头
  12. this.showLoadingState();
  13. // 添加对Bootstrap下拉菜单的初始化
  14. document.addEventListener('DOMContentLoaded', () => {
  15. this.initDropdowns();
  16. });
  17. // 当文档已经加载完成时立即初始化
  18. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  19. this.initDropdowns();
  20. }
  21. return Promise.resolve();
  22. },
  23. // 初始化Bootstrap下拉菜单组件
  24. initDropdowns: function() {
  25. // 减少日志输出
  26. // console.log('[dockerManager] Initializing Bootstrap dropdowns...');
  27. // 直接初始化,不使用setTimeout避免延迟导致的问题
  28. try {
  29. // 动态初始化所有下拉菜单
  30. const dropdownElements = document.querySelectorAll('[data-bs-toggle="dropdown"]');
  31. if (dropdownElements.length === 0) {
  32. return; // 如果没有找到下拉元素,直接返回
  33. }
  34. if (window.bootstrap && window.bootstrap.Dropdown) {
  35. dropdownElements.forEach(el => {
  36. try {
  37. new window.bootstrap.Dropdown(el);
  38. } catch (e) {
  39. // 静默处理错误,不要输出到控制台
  40. }
  41. });
  42. } else {
  43. console.warn('Bootstrap Dropdown 组件未找到,将尝试使用jQuery初始化');
  44. // 尝试使用jQuery初始化(如果存在)
  45. if (window.jQuery) {
  46. window.jQuery('[data-bs-toggle="dropdown"]').dropdown();
  47. }
  48. }
  49. } catch (error) {
  50. // 静默处理错误
  51. }
  52. },
  53. // 显示表格加载状态 - 保持,用于初始渲染和刷新
  54. showLoadingState() {
  55. const table = document.getElementById('dockerStatusTable');
  56. const tbody = document.getElementById('dockerStatusTableBody');
  57. // 首先创建表格标题区域(如果不存在)
  58. let tableContainer = document.getElementById('dockerTableContainer');
  59. if (tableContainer) {
  60. // 添加表格标题区域 - 只有不存在时才添加
  61. if (!tableContainer.querySelector('.docker-table-header')) {
  62. const tableHeader = document.createElement('div');
  63. tableHeader.className = 'docker-table-header';
  64. tableHeader.innerHTML = `
  65. <h2 class="docker-table-title">Docker 容器管理</h2>
  66. <div class="docker-table-actions">
  67. <button id="refreshDockerBtn" class="btn btn-sm btn-primary">
  68. <i class="fas fa-sync-alt me-1"></i> 刷新列表
  69. </button>
  70. </div>
  71. `;
  72. // 插入到表格前面
  73. if (table) {
  74. tableContainer.insertBefore(tableHeader, table);
  75. // 添加刷新按钮事件
  76. const refreshBtn = document.getElementById('refreshDockerBtn');
  77. if (refreshBtn) {
  78. refreshBtn.addEventListener('click', () => {
  79. if (window.systemStatus && typeof window.systemStatus.refreshSystemStatus === 'function') {
  80. window.systemStatus.refreshSystemStatus();
  81. }
  82. });
  83. }
  84. }
  85. }
  86. }
  87. if (table && tbody) {
  88. // 添加Excel风格表格类
  89. table.classList.add('excel-table');
  90. // 确保表头存在并正确渲染
  91. const thead = table.querySelector('thead');
  92. if (thead) {
  93. thead.innerHTML = `
  94. <tr>
  95. <th style="width: 120px;">容器ID</th>
  96. <th style="width: 25%;">容器名称</th>
  97. <th style="width: 35%;">镜像名称</th>
  98. <th style="width: 100px;">运行状态</th>
  99. <th style="width: 150px;">操作</th>
  100. </tr>
  101. `;
  102. }
  103. // 显示加载状态
  104. tbody.innerHTML = `
  105. <tr class="loading-container">
  106. <td colspan="5">
  107. <div class="loading-animation">
  108. <div class="spinner"></div>
  109. <p>正在加载容器列表...</p>
  110. </div>
  111. </td>
  112. </tr>
  113. `;
  114. }
  115. },
  116. // 渲染容器表格 - 核心渲染函数,由 systemStatus 调用
  117. renderContainersTable(containers, dockerStatus) {
  118. // 减少详细日志输出
  119. // console.log(`[dockerManager] Rendering containers table. Containers count: ${containers ? containers.length : 0}`);
  120. const tbody = document.getElementById('dockerStatusTableBody');
  121. if (!tbody) {
  122. return;
  123. }
  124. // 确保表头存在 (showLoadingState 应该已经创建)
  125. const table = document.getElementById('dockerStatusTable');
  126. if (table) {
  127. const thead = table.querySelector('thead');
  128. if (!thead || !thead.querySelector('tr')) {
  129. // 重新创建表头
  130. const newThead = thead || document.createElement('thead');
  131. newThead.innerHTML = `
  132. <tr>
  133. <th style="width: 120px;">容器ID</th>
  134. <th style="width: 25%;">容器名称</th>
  135. <th style="width: 35%;">镜像名称</th>
  136. <th style="width: 100px;">运行状态</th>
  137. <th style="width: 150px;">操作</th>
  138. </tr>
  139. `;
  140. if (!thead) {
  141. table.insertBefore(newThead, tbody);
  142. }
  143. }
  144. }
  145. // 1. 检查 Docker 服务状态
  146. if (dockerStatus !== 'running') {
  147. tbody.innerHTML = `
  148. <tr>
  149. <td colspan="5" class="text-center text-muted py-4">
  150. <i class="fab fa-docker fa-lg me-2"></i> Docker 服务未运行
  151. </td>
  152. </tr>
  153. `;
  154. return;
  155. }
  156. // 2. 检查容器数组是否有效且有内容
  157. if (!Array.isArray(containers) || containers.length === 0) {
  158. tbody.innerHTML = `
  159. <tr>
  160. <td colspan="5" class="text-center text-muted py-4">
  161. <i class="fas fa-info-circle me-2"></i> 暂无运行中的Docker容器
  162. </td>
  163. </tr>
  164. `;
  165. return;
  166. }
  167. // 3. 渲染容器列表
  168. let html = '';
  169. containers.forEach(container => {
  170. const status = container.State || container.status || '未知';
  171. const statusClass = this.getContainerStatusClass(status);
  172. const containerId = container.Id || container.id || '未知';
  173. const containerName = container.Names?.[0]?.substring(1) || container.name || '未知';
  174. const containerImage = container.Image || container.image || '未知';
  175. // 添加lowerStatus变量定义,修复错误
  176. const lowerStatus = status.toLowerCase();
  177. // 替换下拉菜单实现为直接的操作按钮
  178. let actionButtons = '';
  179. // 基本操作:查看日志和详情
  180. actionButtons += `
  181. <button class="btn btn-sm btn-outline-info mb-1 mr-1 action-logs" data-id="${containerId}" data-name="${containerName}">
  182. <i class="fas fa-file-alt"></i> 日志
  183. </button>
  184. <button class="btn btn-sm btn-outline-secondary mb-1 mr-1 action-details" data-id="${containerId}">
  185. <i class="fas fa-info-circle"></i> 详情
  186. </button>
  187. `;
  188. // 根据状态显示不同操作
  189. if (lowerStatus.includes('running')) {
  190. actionButtons += `
  191. <button class="btn btn-sm btn-outline-warning mb-1 mr-1 action-stop" data-id="${containerId}">
  192. <i class="fas fa-stop"></i> 停止
  193. </button>
  194. <button class="btn btn-sm btn-outline-primary mb-1 mr-1 action-restart" data-id="${containerId}">
  195. <i class="fas fa-sync-alt"></i> 重启
  196. </button>
  197. `;
  198. } else if (lowerStatus.includes('exited') || lowerStatus.includes('stopped') || lowerStatus.includes('created')) {
  199. actionButtons += `
  200. <button class="btn btn-sm btn-outline-success mb-1 mr-1 action-start" data-id="${containerId}">
  201. <i class="fas fa-play"></i> 启动
  202. </button>
  203. <button class="btn btn-sm btn-outline-danger mb-1 mr-1 action-remove" data-id="${containerId}">
  204. <i class="fas fa-trash-alt"></i> 删除
  205. </button>
  206. `;
  207. } else if (lowerStatus.includes('paused')) {
  208. actionButtons += `
  209. <button class="btn btn-sm btn-outline-success mb-1 mr-1 action-unpause" data-id="${containerId}">
  210. <i class="fas fa-play"></i> 恢复
  211. </button>
  212. `;
  213. }
  214. // 更新容器按钮(总是显示)
  215. actionButtons += `
  216. <button class="btn btn-sm btn-outline-primary mb-1 mr-1 action-update" data-id="${containerId}" data-image="${containerImage || ''}">
  217. <i class="fas fa-cloud-download-alt"></i> 更新
  218. </button>
  219. `;
  220. html += `
  221. <tr>
  222. <td data-label="ID" title="${containerId}">${containerId.substring(0, 12)}</td>
  223. <td data-label="名称" title="${containerName}">${containerName}</td>
  224. <td data-label="镜像" title="${containerImage}">${containerImage}</td>
  225. <td data-label="状态"><span class="badge ${statusClass}">${status}</span></td>
  226. <td data-label="操作" class="action-cell">
  227. <div class="action-buttons">
  228. ${actionButtons}
  229. </div>
  230. </td>
  231. </tr>
  232. `;
  233. });
  234. tbody.innerHTML = html;
  235. // 为所有操作按钮绑定事件
  236. this.setupButtonListeners();
  237. },
  238. // 为所有操作按钮绑定事件
  239. setupButtonListeners() {
  240. // 查找所有操作按钮并绑定点击事件
  241. document.querySelectorAll('.action-cell button').forEach(button => {
  242. const action = Array.from(button.classList).find(cls => cls.startsWith('action-'));
  243. if (!action) return;
  244. const containerId = button.dataset.id;
  245. if (!containerId) return;
  246. button.addEventListener('click', (event) => {
  247. event.preventDefault();
  248. const containerName = button.dataset.name;
  249. const containerImage = button.dataset.image;
  250. switch (action) {
  251. case 'action-logs':
  252. this.showContainerLogs(containerId, containerName);
  253. break;
  254. case 'action-details':
  255. this.showContainerDetails(containerId);
  256. break;
  257. case 'action-stop':
  258. this.stopContainer(containerId);
  259. break;
  260. case 'action-start':
  261. this.startContainer(containerId);
  262. break;
  263. case 'action-restart':
  264. this.restartContainer(containerId);
  265. break;
  266. case 'action-remove':
  267. this.removeContainer(containerId);
  268. break;
  269. case 'action-unpause':
  270. // this.unpauseContainer(containerId); // 假设有这个函数
  271. console.warn('Unpause action not implemented yet.');
  272. break;
  273. case 'action-update':
  274. this.updateContainer(containerId, containerImage);
  275. break;
  276. default:
  277. console.warn('Unknown action:', action);
  278. }
  279. });
  280. });
  281. },
  282. // 获取容器状态对应的 CSS 类 - 保持
  283. getContainerStatusClass(state) {
  284. if (!state) return 'status-unknown';
  285. state = state.toLowerCase();
  286. if (state.includes('running')) return 'status-running';
  287. if (state.includes('created')) return 'status-created';
  288. if (state.includes('exited') || state.includes('stopped')) return 'status-stopped';
  289. if (state.includes('paused')) return 'status-paused';
  290. return 'status-unknown';
  291. },
  292. // 设置下拉菜单动作的事件监听 (委托方法 - 现在直接使用按钮,不再需要)
  293. setupActionDropdownListener() {
  294. // 这个方法留作兼容性,但实际上我们现在直接使用按钮而非下拉菜单
  295. },
  296. // 查看日志 (示例:用 SweetAlert 显示)
  297. async showContainerLogs(containerId, containerName) {
  298. core.showLoading('正在加载日志...');
  299. try {
  300. // 注意: 后端 /api/docker/containers/:id/logs 需要存在并返回日志文本
  301. const response = await fetch(`/api/docker/containers/${containerId}/logs`);
  302. if (!response.ok) {
  303. const errorData = await response.json().catch(() => ({ details: '无法解析错误响应' }));
  304. throw new Error(errorData.details || `获取日志失败 (${response.status})`);
  305. }
  306. const logs = await response.text();
  307. core.hideLoading();
  308. Swal.fire({
  309. title: `容器日志: ${containerName || containerId.substring(0, 6)}`,
  310. html: `<pre class="container-logs">${logs.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</pre>`,
  311. width: '80%',
  312. customClass: {
  313. htmlContainer: 'swal2-logs-container',
  314. popup: 'swal2-logs-popup'
  315. },
  316. confirmButtonText: '关闭'
  317. });
  318. } catch (error) {
  319. core.hideLoading();
  320. core.showAlert(`查看日志失败: ${error.message}`, 'error');
  321. logger.error(`[dockerManager] Error fetching logs for ${containerId}:`, error);
  322. }
  323. },
  324. // 显示容器详情 (示例:用 SweetAlert 显示)
  325. async showContainerDetails(containerId) {
  326. core.showLoading('正在加载详情...');
  327. try {
  328. // 注意: 后端 /api/docker/containers/:id 需要存在并返回详细信息
  329. const response = await fetch(`/api/docker/containers/${containerId}`);
  330. if (!response.ok) {
  331. const errorData = await response.json().catch(() => ({ details: '无法解析错误响应' }));
  332. throw new Error(errorData.details || `获取详情失败 (${response.status})`);
  333. }
  334. const details = await response.json();
  335. core.hideLoading();
  336. // 格式化显示详情
  337. let detailsHtml = '<div class="container-details">';
  338. for (const key in details) {
  339. detailsHtml += `<p><strong>${key}:</strong> ${JSON.stringify(details[key], null, 2)}</p>`;
  340. }
  341. detailsHtml += '</div>';
  342. Swal.fire({
  343. title: `容器详情: ${details.Name || containerId.substring(0, 6)}`,
  344. html: detailsHtml,
  345. width: '80%',
  346. confirmButtonText: '关闭'
  347. });
  348. } catch (error) {
  349. core.hideLoading();
  350. core.showAlert(`查看详情失败: ${error.message}`, 'error');
  351. logger.error(`[dockerManager] Error fetching details for ${containerId}:`, error);
  352. }
  353. },
  354. // 启动容器
  355. async startContainer(containerId) {
  356. core.showLoading('正在启动容器...');
  357. try {
  358. const response = await fetch(`/api/docker/containers/${containerId}/start`, { method: 'POST' });
  359. const data = await response.json();
  360. core.hideLoading();
  361. if (!response.ok) throw new Error(data.details || '启动容器失败');
  362. core.showAlert('容器启动成功', 'success');
  363. systemStatus.refreshSystemStatus(); // 刷新整体状态
  364. } catch (error) {
  365. core.hideLoading();
  366. core.showAlert(`启动容器失败: ${error.message}`, 'error');
  367. logger.error(`[dockerManager] Error starting container ${containerId}:`, error);
  368. }
  369. },
  370. // 停止容器
  371. async stopContainer(containerId) {
  372. core.showLoading('正在停止容器...');
  373. try {
  374. const response = await fetch(`/api/docker/containers/${containerId}/stop`, { method: 'POST' });
  375. const data = await response.json();
  376. core.hideLoading();
  377. if (!response.ok && response.status !== 304) { // 304 Not Modified 也算成功(已停止)
  378. throw new Error(data.details || '停止容器失败');
  379. }
  380. core.showAlert(data.message || '容器停止成功', 'success');
  381. systemStatus.refreshSystemStatus(); // 刷新整体状态
  382. } catch (error) {
  383. core.hideLoading();
  384. core.showAlert(`停止容器失败: ${error.message}`, 'error');
  385. logger.error(`[dockerManager] Error stopping container ${containerId}:`, error);
  386. }
  387. },
  388. // 重启容器
  389. async restartContainer(containerId) {
  390. core.showLoading('正在重启容器...');
  391. try {
  392. const response = await fetch(`/api/docker/containers/${containerId}/restart`, { method: 'POST' });
  393. const data = await response.json();
  394. core.hideLoading();
  395. if (!response.ok) throw new Error(data.details || '重启容器失败');
  396. core.showAlert('容器重启成功', 'success');
  397. systemStatus.refreshSystemStatus(); // 刷新整体状态
  398. } catch (error) {
  399. core.hideLoading();
  400. core.showAlert(`重启容器失败: ${error.message}`, 'error');
  401. logger.error(`[dockerManager] Error restarting container ${containerId}:`, error);
  402. }
  403. },
  404. // 删除容器 (带确认)
  405. removeContainer(containerId) {
  406. Swal.fire({
  407. title: '确认删除?',
  408. text: `确定要删除容器 ${containerId.substring(0, 6)} 吗?此操作不可恢复!`,
  409. icon: 'warning',
  410. showCancelButton: true,
  411. confirmButtonColor: 'var(--danger-color)',
  412. cancelButtonColor: '#6c757d',
  413. confirmButtonText: '确认删除',
  414. cancelButtonText: '取消'
  415. }).then(async (result) => {
  416. if (result.isConfirmed) {
  417. core.showLoading('正在删除容器...');
  418. try {
  419. const response = await fetch(`/api/docker/containers/${containerId}/remove`, { method: 'POST' }); // 使用 remove
  420. const data = await response.json();
  421. core.hideLoading();
  422. if (!response.ok) throw new Error(data.details || '删除容器失败');
  423. core.showAlert(data.message || '容器删除成功', 'success');
  424. systemStatus.refreshSystemStatus(); // 刷新整体状态
  425. } catch (error) {
  426. core.hideLoading();
  427. core.showAlert(`删除容器失败: ${error.message}`, 'error');
  428. logger.error(`[dockerManager] Error removing container ${containerId}:`, error);
  429. }
  430. }
  431. });
  432. },
  433. // --- 新增:更新容器函数 ---
  434. async updateContainer(containerId, currentImage) {
  435. const imageName = currentImage.split(':')[0]; // 提取基础镜像名
  436. const { value: newTag } = await Swal.fire({
  437. title: `更新容器: ${imageName}`,
  438. input: 'text',
  439. inputLabel: '请输入新的镜像标签 (例如 latest, v1.2)',
  440. inputValue: 'latest', // 默认值
  441. showCancelButton: true,
  442. confirmButtonText: '开始更新',
  443. cancelButtonText: '取消',
  444. confirmButtonColor: '#3085d6',
  445. cancelButtonColor: '#d33',
  446. inputValidator: (value) => {
  447. if (!value || value.trim() === '') {
  448. return '镜像标签不能为空!';
  449. }
  450. },
  451. // 美化弹窗样式
  452. customClass: {
  453. container: 'update-container',
  454. popup: 'update-popup',
  455. header: 'update-header',
  456. title: 'update-title',
  457. closeButton: 'update-close',
  458. icon: 'update-icon',
  459. image: 'update-image',
  460. content: 'update-content',
  461. input: 'update-input',
  462. actions: 'update-actions',
  463. confirmButton: 'update-confirm',
  464. cancelButton: 'update-cancel',
  465. footer: 'update-footer'
  466. }
  467. });
  468. if (newTag) {
  469. // 显示进度弹窗
  470. Swal.fire({
  471. title: '更新容器',
  472. html: `
  473. <div class="update-progress">
  474. <p>正在更新容器 <strong>${containerId.substring(0, 8)}</strong></p>
  475. <p>镜像: <strong>${imageName}:${newTag.trim()}</strong></p>
  476. <div class="progress-status">准备中...</div>
  477. <div class="progress-container">
  478. <div class="progress-bar"></div>
  479. </div>
  480. </div>
  481. `,
  482. showConfirmButton: false,
  483. allowOutsideClick: false,
  484. allowEscapeKey: false,
  485. didOpen: () => {
  486. const progressBar = Swal.getPopup().querySelector('.progress-bar');
  487. const progressStatus = Swal.getPopup().querySelector('.progress-status');
  488. // 设置初始进度
  489. progressBar.style.width = '0%';
  490. progressBar.style.backgroundColor = '#4CAF50';
  491. // 模拟进度动画
  492. let progress = 0;
  493. const progressInterval = setInterval(() => {
  494. // 进度最多到95%,剩下的在请求完成后处理
  495. if (progress < 95) {
  496. progress += Math.random() * 3;
  497. if (progress > 95) progress = 95;
  498. progressBar.style.width = `${progress}%`;
  499. // 更新状态文本
  500. if (progress < 30) {
  501. progressStatus.textContent = "拉取新镜像...";
  502. } else if (progress < 60) {
  503. progressStatus.textContent = "准备更新容器...";
  504. } else if (progress < 90) {
  505. progressStatus.textContent = "应用新配置...";
  506. } else {
  507. progressStatus.textContent = "即将完成...";
  508. }
  509. }
  510. }, 300);
  511. // 发送更新请求
  512. this.performContainerUpdate(containerId, newTag.trim(), progressBar, progressStatus, progressInterval);
  513. }
  514. });
  515. }
  516. },
  517. // 执行容器更新请求
  518. async performContainerUpdate(containerId, newTag, progressBar, progressStatus, progressInterval) {
  519. try {
  520. const response = await fetch(`/api/docker/containers/${containerId}/update`, {
  521. method: 'POST',
  522. headers: {
  523. 'Content-Type': 'application/json'
  524. },
  525. body: JSON.stringify({ tag: newTag })
  526. });
  527. // 清除进度定时器
  528. clearInterval(progressInterval);
  529. if (response.ok) {
  530. const data = await response.json();
  531. // 设置进度为100%
  532. progressBar.style.width = '100%';
  533. progressStatus.textContent = "更新完成!";
  534. // 显示成功消息
  535. setTimeout(() => {
  536. Swal.fire({
  537. icon: 'success',
  538. title: '更新成功!',
  539. text: data.message || '容器已成功更新',
  540. confirmButtonText: '确定'
  541. });
  542. // 刷新容器列表
  543. systemStatus.refreshSystemStatus();
  544. }, 800);
  545. } else {
  546. const data = await response.json().catch(() => ({ error: '解析响应失败', details: '服务器返回了无效的数据' }));
  547. // 设置进度条为错误状态
  548. progressBar.style.width = '100%';
  549. progressBar.style.backgroundColor = '#f44336';
  550. progressStatus.textContent = "更新失败";
  551. // 显示错误消息
  552. setTimeout(() => {
  553. Swal.fire({
  554. icon: 'error',
  555. title: '更新失败',
  556. text: data.details || data.error || '未知错误',
  557. confirmButtonText: '确定'
  558. });
  559. }, 800);
  560. }
  561. } catch (error) {
  562. // 清除进度定时器
  563. clearInterval(progressInterval);
  564. // 设置进度条为错误状态
  565. progressBar.style.width = '100%';
  566. progressBar.style.backgroundColor = '#f44336';
  567. progressStatus.textContent = "更新出错";
  568. // 显示错误信息
  569. setTimeout(() => {
  570. Swal.fire({
  571. icon: 'error',
  572. title: '更新失败',
  573. text: error.message || '网络请求失败',
  574. confirmButtonText: '确定'
  575. });
  576. }, 800);
  577. // 记录错误日志
  578. logger.error(`[dockerManager] Error updating container ${containerId} to tag ${newTag}:`, error);
  579. }
  580. },
  581. // --- 新增:绑定排查按钮事件 ---
  582. bindTroubleshootButton() {
  583. // 使用 setTimeout 确保按钮已经渲染到 DOM 中
  584. setTimeout(() => {
  585. const troubleshootBtn = document.getElementById('docker-troubleshoot-btn');
  586. if (troubleshootBtn) {
  587. // 先移除旧监听器,防止重复绑定
  588. troubleshootBtn.replaceWith(troubleshootBtn.cloneNode(true));
  589. const newBtn = document.getElementById('docker-troubleshoot-btn'); // 重新获取克隆后的按钮
  590. if(newBtn) {
  591. newBtn.addEventListener('click', () => {
  592. if (window.systemStatus && typeof window.systemStatus.showDockerHelp === 'function') {
  593. window.systemStatus.showDockerHelp();
  594. } else {
  595. console.error('[dockerManager] systemStatus.showDockerHelp is not available.');
  596. // 可以提供一个备用提示
  597. alert('无法显示帮助信息,请检查控制台。');
  598. }
  599. });
  600. console.log('[dockerManager] Troubleshoot button event listener bound.');
  601. } else {
  602. console.warn('[dockerManager] Cloned troubleshoot button not found after replace.');
  603. }
  604. } else {
  605. console.warn('[dockerManager] Troubleshoot button not found for binding.');
  606. }
  607. }, 0); // 延迟 0ms 执行,让浏览器有机会渲染
  608. }
  609. };
  610. // 确保在 DOM 加载后初始化
  611. document.addEventListener('DOMContentLoaded', () => {
  612. // 注意:init 现在只设置监听器,不加载数据
  613. // dockerManager.init();
  614. // 可以在 app.js 或 systemStatus.js 初始化完成后调用
  615. });