dockerManager.js 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071
  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. try {
  26. console.log('[dockerManager] 初始化下拉菜单...');
  27. // 动态初始化所有下拉菜单按钮
  28. const dropdownElements = document.querySelectorAll('[data-bs-toggle="dropdown"]');
  29. console.log(`[dockerManager] 找到 ${dropdownElements.length} 个下拉元素`);
  30. if (dropdownElements.length === 0) {
  31. return; // 如果没有找到下拉元素,直接返回
  32. }
  33. // 尝试使用所有可能的Bootstrap初始化方法
  34. if (window.bootstrap && typeof window.bootstrap.Dropdown !== 'undefined') {
  35. console.log('[dockerManager] 使用 Bootstrap 5 初始化下拉菜单');
  36. dropdownElements.forEach(el => {
  37. try {
  38. new window.bootstrap.Dropdown(el);
  39. } catch (e) {
  40. console.error('Bootstrap 5 下拉菜单初始化错误:', e);
  41. }
  42. });
  43. } else if (typeof $ !== 'undefined' && typeof $.fn.dropdown !== 'undefined') {
  44. console.log('[dockerManager] 使用 jQuery Bootstrap 初始化下拉菜单');
  45. $(dropdownElements).dropdown();
  46. } else {
  47. console.warn('[dockerManager] 未找到Bootstrap下拉菜单组件,将使用手动下拉实现');
  48. this.setupManualDropdowns();
  49. }
  50. } catch (error) {
  51. console.error('[dockerManager] 初始化下拉菜单错误:', error);
  52. // 失败时使用备用方案
  53. this.setupManualDropdowns();
  54. }
  55. },
  56. // 手动实现下拉菜单功能(备用方案)
  57. setupManualDropdowns: function() {
  58. console.log('[dockerManager] 设置手动下拉菜单...');
  59. // 为所有下拉菜单按钮添加点击事件
  60. document.querySelectorAll('.btn-group .dropdown-toggle').forEach(button => {
  61. // 移除旧事件监听器
  62. const newButton = button.cloneNode(true);
  63. button.parentNode.replaceChild(newButton, button);
  64. // 添加新事件监听器
  65. newButton.addEventListener('click', function(e) {
  66. e.preventDefault();
  67. e.stopPropagation();
  68. // 查找关联的下拉菜单
  69. const dropdownMenu = this.nextElementSibling;
  70. if (!dropdownMenu || !dropdownMenu.classList.contains('dropdown-menu')) {
  71. return;
  72. }
  73. // 切换显示/隐藏
  74. const isVisible = dropdownMenu.classList.contains('show');
  75. // 先隐藏所有其他打开的下拉菜单
  76. document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
  77. menu.classList.remove('show');
  78. });
  79. // 切换当前菜单
  80. if (!isVisible) {
  81. dropdownMenu.classList.add('show');
  82. // 计算位置 - 精确计算确保菜单位置更美观
  83. const buttonRect = newButton.getBoundingClientRect();
  84. const tableCell = newButton.closest('td');
  85. const tableCellRect = tableCell ? tableCell.getBoundingClientRect() : buttonRect;
  86. // 设置最小宽度,确保下拉菜单够宽
  87. const minWidth = Math.max(180, buttonRect.width * 1.5);
  88. dropdownMenu.style.minWidth = `${minWidth}px`;
  89. // 设置绝对定位
  90. dropdownMenu.style.position = 'absolute';
  91. // 根据屏幕空间计算最佳位置
  92. const viewportWidth = window.innerWidth;
  93. const viewportHeight = window.innerHeight;
  94. const spaceRight = viewportWidth - buttonRect.right;
  95. const spaceBottom = viewportHeight - buttonRect.bottom;
  96. const spaceAbove = buttonRect.top;
  97. // 先移除所有位置相关的类
  98. dropdownMenu.classList.remove('dropdown-menu-top', 'dropdown-menu-right');
  99. // 设置为右对齐,且显示在按钮上方
  100. dropdownMenu.style.right = '0';
  101. dropdownMenu.style.left = 'auto';
  102. // 计算菜单高度 (假设每个菜单项高度为40px,分隔线10px)
  103. const menuItemCount = dropdownMenu.querySelectorAll('.dropdown-item').length;
  104. const dividerCount = dropdownMenu.querySelectorAll('.dropdown-divider').length;
  105. const estimatedMenuHeight = (menuItemCount * 40) + (dividerCount * 10) + 20; // 加上padding
  106. // 优先显示在按钮上方,如果空间不足则显示在下方
  107. if (spaceAbove >= estimatedMenuHeight && spaceAbove > spaceBottom) {
  108. // 显示在按钮上方
  109. dropdownMenu.style.bottom = `${buttonRect.height + 5}px`; // 5px间距
  110. dropdownMenu.style.top = 'auto';
  111. // 设置动画原点为底部
  112. dropdownMenu.style.transformOrigin = 'bottom right';
  113. // 添加上方显示的类
  114. dropdownMenu.classList.add('dropdown-menu-top');
  115. } else {
  116. // 显示在右侧而不是正下方
  117. if (spaceRight >= minWidth && tableCellRect.width > buttonRect.width + 20) {
  118. // 有足够的右侧空间,显示在按钮右侧
  119. dropdownMenu.style.top = '0';
  120. dropdownMenu.style.left = `${buttonRect.width + 5}px`; // 5px间距
  121. dropdownMenu.style.right = 'auto';
  122. dropdownMenu.style.bottom = 'auto';
  123. dropdownMenu.style.transformOrigin = 'left top';
  124. // 添加右侧显示的类
  125. dropdownMenu.classList.add('dropdown-menu-right');
  126. } else {
  127. // 显示在按钮下方,但尝试右对齐
  128. dropdownMenu.style.top = `${buttonRect.height + 5}px`; // 5px间距
  129. dropdownMenu.style.bottom = 'auto';
  130. // 如果下拉菜单宽度超过右侧可用空间,则左对齐显示
  131. if (minWidth > spaceRight) {
  132. dropdownMenu.style.right = 'auto';
  133. dropdownMenu.style.left = '0';
  134. } else {
  135. // 继续使用右对齐
  136. dropdownMenu.classList.add('dropdown-menu-end');
  137. }
  138. dropdownMenu.style.transformOrigin = 'top right';
  139. }
  140. }
  141. // 清除其他可能影响布局的样式
  142. dropdownMenu.style.margin = '0';
  143. dropdownMenu.style.maxHeight = '85vh';
  144. dropdownMenu.style.overflowY = 'auto';
  145. dropdownMenu.style.zIndex = '1050'; // 确保在表格上方
  146. }
  147. // 点击其他区域关闭下拉菜单
  148. const closeHandler = function(event) {
  149. if (!dropdownMenu.contains(event.target) && !newButton.contains(event.target)) {
  150. dropdownMenu.classList.remove('show');
  151. document.removeEventListener('click', closeHandler);
  152. }
  153. };
  154. // 只在打开菜单时添加全局点击监听
  155. if (!isVisible) {
  156. // 延迟一点添加事件,避免立即触发
  157. setTimeout(() => {
  158. document.addEventListener('click', closeHandler);
  159. }, 10);
  160. }
  161. });
  162. });
  163. },
  164. // 显示表格加载状态 - 保持,用于初始渲染和刷新
  165. showLoadingState() {
  166. const table = document.getElementById('dockerStatusTable');
  167. const tbody = document.getElementById('dockerStatusTableBody');
  168. // 首先创建表格标题区域(如果不存在)
  169. let tableContainer = document.getElementById('dockerTableContainer');
  170. if (tableContainer) {
  171. // 添加表格标题区域 - 只有不存在时才添加
  172. if (!tableContainer.querySelector('.docker-table-header')) {
  173. const tableHeader = document.createElement('div');
  174. tableHeader.className = 'docker-table-header';
  175. tableHeader.innerHTML = `
  176. <h2 class="docker-table-title">Docker 容器管理</h2>
  177. <div class="docker-table-actions">
  178. <button id="refreshDockerBtn" class="btn btn-sm btn-primary">
  179. <i class="fas fa-sync-alt me-1"></i> 刷新列表
  180. </button>
  181. </div>
  182. `;
  183. // 插入到表格前面
  184. if (table) {
  185. tableContainer.insertBefore(tableHeader, table);
  186. // 添加刷新按钮事件
  187. const refreshBtn = document.getElementById('refreshDockerBtn');
  188. if (refreshBtn) {
  189. refreshBtn.addEventListener('click', () => {
  190. // 显示加载状态,提高用户体验
  191. this.showRefreshingState(refreshBtn);
  192. if (window.systemStatus && typeof window.systemStatus.refreshSystemStatus === 'function') {
  193. window.systemStatus.refreshSystemStatus();
  194. }
  195. });
  196. }
  197. }
  198. }
  199. }
  200. if (table && tbody) {
  201. // 添加Excel风格表格类
  202. table.classList.add('excel-table');
  203. // 确保表头存在并正确渲染
  204. const thead = table.querySelector('thead');
  205. if (thead) {
  206. thead.innerHTML = `
  207. <tr>
  208. <th style="width: 12%;">容器ID</th>
  209. <th style="width: 18%;">容器名称</th>
  210. <th style="width: 30%;">镜像名称</th>
  211. <th style="width: 15%;">运行状态</th>
  212. <th style="width: 15%;">操作</th>
  213. </tr>
  214. `;
  215. }
  216. // 显示加载状态
  217. tbody.innerHTML = `
  218. <tr class="loading-container">
  219. <td colspan="5">
  220. <div class="loading-animation">
  221. <div class="spinner"></div>
  222. <p>正在加载容器列表...</p>
  223. </div>
  224. </td>
  225. </tr>
  226. `;
  227. // 添加表格样式
  228. this.applyTableStyles(table);
  229. }
  230. },
  231. // 新增:显示刷新中状态
  232. showRefreshingState(refreshBtn) {
  233. if (!refreshBtn) return;
  234. // 保存原始按钮内容
  235. const originalContent = refreshBtn.innerHTML;
  236. // 更改为加载状态
  237. refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> 刷新中...';
  238. refreshBtn.disabled = true;
  239. refreshBtn.classList.add('refreshing');
  240. // 添加样式使按钮看起来正在加载
  241. const style = document.createElement('style');
  242. style.textContent = `
  243. .btn.refreshing {
  244. opacity: 0.8;
  245. cursor: not-allowed;
  246. }
  247. @keyframes pulse {
  248. 0% { opacity: 0.6; }
  249. 50% { opacity: 1; }
  250. 100% { opacity: 0.6; }
  251. }
  252. .btn.refreshing i {
  253. animation: pulse 1.5s infinite;
  254. }
  255. .table-overlay {
  256. position: absolute;
  257. top: 0;
  258. left: 0;
  259. right: 0;
  260. bottom: 0;
  261. background-color: rgba(255, 255, 255, 0.7);
  262. display: flex;
  263. flex-direction: column;
  264. justify-content: center;
  265. align-items: center;
  266. z-index: 10;
  267. border-radius: 0.25rem;
  268. }
  269. .table-overlay .spinner {
  270. width: 40px;
  271. height: 40px;
  272. border: 4px solid #f3f3f3;
  273. border-top: 4px solid #3498db;
  274. border-radius: 50%;
  275. animation: spin 1s linear infinite;
  276. margin-bottom: 10px;
  277. }
  278. @keyframes spin {
  279. 0% { transform: rotate(0deg); }
  280. 100% { transform: rotate(360deg); }
  281. }
  282. `;
  283. // 检查是否已经添加了样式
  284. const existingStyle = document.querySelector('style[data-for="refresh-button"]');
  285. if (!existingStyle) {
  286. style.setAttribute('data-for', 'refresh-button');
  287. document.head.appendChild(style);
  288. }
  289. // 获取表格和容器
  290. const table = document.getElementById('dockerStatusTable');
  291. const tableContainer = document.getElementById('dockerTableContainer');
  292. // 移除任何现有的覆盖层
  293. const existingOverlay = document.querySelector('.table-overlay');
  294. if (existingOverlay) {
  295. existingOverlay.remove();
  296. }
  297. // 创建一个覆盖层而不是替换表格内容
  298. if (table) {
  299. // 设置表格容器为相对定位,以便正确放置覆盖层
  300. if (tableContainer) {
  301. tableContainer.style.position = 'relative';
  302. } else {
  303. table.parentNode.style.position = 'relative';
  304. }
  305. // 创建覆盖层
  306. const overlay = document.createElement('div');
  307. overlay.className = 'table-overlay';
  308. overlay.innerHTML = `
  309. <div class="spinner"></div>
  310. <p>正在更新容器列表...</p>
  311. `;
  312. // 获取表格的位置并设置覆盖层
  313. const tableRect = table.getBoundingClientRect();
  314. overlay.style.width = `${table.offsetWidth}px`;
  315. overlay.style.height = `${table.offsetHeight}px`;
  316. // 将覆盖层添加到表格容器
  317. if (tableContainer) {
  318. tableContainer.appendChild(overlay);
  319. } else {
  320. table.parentNode.appendChild(overlay);
  321. }
  322. }
  323. // 设置超时,防止永久加载状态
  324. setTimeout(() => {
  325. // 如果按钮仍处于加载状态,恢复为原始状态
  326. if (refreshBtn.classList.contains('refreshing')) {
  327. refreshBtn.innerHTML = originalContent;
  328. refreshBtn.disabled = false;
  329. refreshBtn.classList.remove('refreshing');
  330. // 移除覆盖层
  331. const overlay = document.querySelector('.table-overlay');
  332. if (overlay) {
  333. overlay.remove();
  334. }
  335. }
  336. }, 10000); // 10秒超时
  337. },
  338. // 渲染容器表格 - 核心渲染函数,由 systemStatus 调用
  339. renderContainersTable(containers, dockerStatus) {
  340. // 减少详细日志输出
  341. // console.log(`[dockerManager] Rendering containers table. Containers count: ${containers ? containers.length : 0}`);
  342. // 重置刷新按钮状态
  343. const refreshBtn = document.getElementById('refreshDockerBtn');
  344. if (refreshBtn && refreshBtn.classList.contains('refreshing')) {
  345. refreshBtn.innerHTML = '<i class="fas fa-sync-alt me-1"></i> 刷新列表';
  346. refreshBtn.disabled = false;
  347. refreshBtn.classList.remove('refreshing');
  348. // 移除覆盖层
  349. const overlay = document.querySelector('.table-overlay');
  350. if (overlay) {
  351. overlay.remove();
  352. }
  353. }
  354. const tbody = document.getElementById('dockerStatusTableBody');
  355. if (!tbody) {
  356. return;
  357. }
  358. // 确保表头存在 (showLoadingState 应该已经创建)
  359. const table = document.getElementById('dockerStatusTable');
  360. if (table) {
  361. const thead = table.querySelector('thead');
  362. if (!thead || !thead.querySelector('tr')) {
  363. // 重新创建表头
  364. const newThead = thead || document.createElement('thead');
  365. newThead.innerHTML = `
  366. <tr>
  367. <th style="width: 12%;">容器ID</th>
  368. <th style="width: 18%;">容器名称</th>
  369. <th style="width: 30%;">镜像名称</th>
  370. <th style="width: 15%;">运行状态</th>
  371. <th style="width: 15%;">操作</th>
  372. </tr>
  373. `;
  374. if (!thead) {
  375. table.insertBefore(newThead, tbody);
  376. }
  377. }
  378. // 应用表格样式
  379. this.applyTableStyles(table);
  380. }
  381. // 1. 检查 Docker 服务状态
  382. if (dockerStatus !== 'running') {
  383. tbody.innerHTML = `
  384. <tr>
  385. <td colspan="5" class="text-center text-muted py-4">
  386. <i class="fab fa-docker fa-lg me-2"></i> Docker 服务未运行
  387. </td>
  388. </tr>
  389. `;
  390. return;
  391. }
  392. // 2. 检查容器数组是否有效且有内容
  393. if (!Array.isArray(containers) || containers.length === 0) {
  394. tbody.innerHTML = `
  395. <tr>
  396. <td colspan="5" class="text-center text-muted py-4">
  397. <i class="fas fa-info-circle me-2"></i> 暂无运行中的Docker容器
  398. </td>
  399. </tr>
  400. `;
  401. return;
  402. }
  403. // 3. 渲染容器列表
  404. let html = '';
  405. containers.forEach(container => {
  406. const status = container.State || container.status || '未知';
  407. const statusClass = this.getContainerStatusClass(status);
  408. const containerId = container.Id || container.id || '未知';
  409. const containerName = container.Names?.[0]?.substring(1) || container.name || '未知';
  410. const containerImage = container.Image || container.image || '未知';
  411. // 添加lowerStatus变量定义,修复错误
  412. const lowerStatus = status.toLowerCase();
  413. // 创建按钮组,使用标准Bootstrap 5下拉菜单语法
  414. let actionButtons = `
  415. <div class="btn-group">
  416. <button type="button" class="btn btn-sm btn-primary dropdown-toggle simple-dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
  417. 操作
  418. </button>
  419. <select class="simple-dropdown">
  420. <option value="" selected disabled>选择操作</option>
  421. <option class="dropdown-item action-logs" data-id="${containerId}" data-name="${containerName}">查看日志</option>
  422. `;
  423. // 根据状态添加不同的操作选项
  424. if (lowerStatus.includes('running')) {
  425. actionButtons += `
  426. <option class="dropdown-item action-stop" data-id="${containerId}">停止容器</option>
  427. `;
  428. } else if (lowerStatus.includes('exited') || lowerStatus.includes('stopped') || lowerStatus.includes('created')) {
  429. actionButtons += `
  430. <option class="dropdown-item action-start" data-id="${containerId}">启动容器</option>
  431. `;
  432. } else if (lowerStatus.includes('paused')) {
  433. actionButtons += `
  434. <option class="dropdown-item action-unpause" data-id="${containerId}">恢复容器</option>
  435. `;
  436. }
  437. // 重启和删除操作对所有状态都可用
  438. actionButtons += `
  439. <option class="dropdown-item action-restart" data-id="${containerId}">重启容器</option>
  440. <option class="dropdown-item action-stop" data-id="${containerId}">停止容器</option>
  441. <option class="dropdown-item action-remove" data-id="${containerId}">删除容器</option>
  442. <option class="dropdown-item action-update" data-id="${containerId}" data-image="${containerImage || ''}">更新容器</option>
  443. </select>
  444. </div>
  445. `;
  446. html += `
  447. <tr>
  448. <td data-label="ID" title="${containerId}" class="text-center">${containerId.substring(0, 12)}</td>
  449. <td data-label="名称" title="${containerName}" class="text-center">${containerName}</td>
  450. <td data-label="镜像" title="${containerImage}" class="text-center">${containerImage}</td>
  451. <td data-label="状态" class="text-center"><span class="badge ${statusClass}">${status}</span></td>
  452. <td data-label="操作" class="action-cell text-center">
  453. <div class="action-buttons">
  454. ${actionButtons}
  455. </div>
  456. </td>
  457. </tr>
  458. `;
  459. });
  460. tbody.innerHTML = html;
  461. // 为所有操作按钮绑定事件
  462. this.setupButtonListeners();
  463. // 确保在内容渲染后立即初始化下拉菜单
  464. setTimeout(() => {
  465. this.initDropdowns();
  466. // 备用方法:直接为下拉菜单按钮添加点击事件
  467. this.setupManualDropdowns();
  468. }, 100); // 增加延迟确保DOM完全渲染
  469. },
  470. // 为所有操作按钮绑定事件 - 简化此方法,专注于直接点击处理
  471. setupButtonListeners() {
  472. // 为下拉框选择事件添加处理逻辑
  473. document.querySelectorAll('.action-cell .simple-dropdown').forEach(select => {
  474. select.addEventListener('change', (event) => {
  475. event.preventDefault();
  476. const selectedOption = select.options[select.selectedIndex];
  477. if (!selectedOption || selectedOption.disabled) return;
  478. const action = Array.from(selectedOption.classList).find(cls => cls.startsWith('action-'));
  479. if (!action) return;
  480. const containerId = selectedOption.getAttribute('data-id');
  481. if (!containerId) return;
  482. const containerName = selectedOption.getAttribute('data-name');
  483. const containerImage = selectedOption.getAttribute('data-image');
  484. console.log('处理容器操作:', action, '容器ID:', containerId);
  485. // 执行对应的容器操作
  486. this.handleContainerAction(action, containerId, containerName, containerImage);
  487. // 重置选择,以便下次可以再次选择相同选项
  488. select.selectedIndex = 0;
  489. });
  490. });
  491. // 让下拉框按钮隐藏,只显示select元素
  492. document.querySelectorAll('.simple-dropdown-toggle').forEach(button => {
  493. button.style.display = 'none';
  494. });
  495. // 样式化select元素
  496. document.querySelectorAll('.simple-dropdown').forEach(select => {
  497. select.style.display = 'block';
  498. select.style.width = '100%';
  499. select.style.padding = '0.375rem 0.75rem';
  500. select.style.fontSize = '0.875rem';
  501. select.style.borderRadius = '0.25rem';
  502. select.style.border = '1px solid #ced4da';
  503. select.style.backgroundColor = '#fff';
  504. });
  505. },
  506. // 处理容器操作的统一方法
  507. handleContainerAction(action, containerId, containerName, containerImage) {
  508. console.log('Handling container action:', action, 'for container:', containerId);
  509. switch (action) {
  510. case 'action-logs':
  511. this.showContainerLogs(containerId, containerName);
  512. break;
  513. case 'action-stop':
  514. this.stopContainer(containerId);
  515. break;
  516. case 'action-start':
  517. this.startContainer(containerId);
  518. break;
  519. case 'action-restart':
  520. this.restartContainer(containerId);
  521. break;
  522. case 'action-remove':
  523. this.removeContainer(containerId);
  524. break;
  525. case 'action-unpause':
  526. console.warn('Unpause action not implemented yet.');
  527. break;
  528. case 'action-update':
  529. this.updateContainer(containerId, containerImage);
  530. break;
  531. default:
  532. console.warn('Unknown action:', action);
  533. }
  534. },
  535. // 获取容器状态对应的 CSS 类 - 保持
  536. getContainerStatusClass(state) {
  537. if (!state) return 'status-unknown';
  538. state = state.toLowerCase();
  539. if (state.includes('running')) return 'status-running';
  540. if (state.includes('created')) return 'status-created';
  541. if (state.includes('exited') || state.includes('stopped')) return 'status-stopped';
  542. if (state.includes('paused')) return 'status-paused';
  543. return 'status-unknown';
  544. },
  545. // 设置下拉菜单动作的事件监听 - 简化为空方法,因为使用原生select不需要
  546. setupActionDropdownListener() {
  547. // 不需要特殊处理,使用原生select元素的change事件
  548. },
  549. // 查看日志
  550. async showContainerLogs(containerId, containerName) {
  551. console.log('正在获取日志,容器ID:', containerId, '容器名称:', containerName);
  552. core.showLoading('正在加载日志...');
  553. try {
  554. // 注意: 后端 /api/docker/containers/:id/logs 需要存在并返回日志文本
  555. const response = await fetch(`/api/docker/containers/${containerId}/logs`);
  556. if (!response.ok) {
  557. const errorData = await response.json().catch(() => ({ details: '无法解析错误响应' }));
  558. throw new Error(errorData.details || `获取日志失败 (${response.status})`);
  559. }
  560. const logs = await response.text();
  561. core.hideLoading();
  562. Swal.fire({
  563. title: `容器日志: ${containerName || containerId.substring(0, 6)}`,
  564. html: `<pre class="container-logs">${logs.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</pre>`,
  565. width: '80%',
  566. customClass: {
  567. htmlContainer: 'swal2-logs-container',
  568. popup: 'swal2-logs-popup'
  569. },
  570. confirmButtonText: '关闭'
  571. });
  572. } catch (error) {
  573. core.hideLoading();
  574. core.showAlert(`查看日志失败: ${error.message}`, 'error');
  575. console.error(`[dockerManager] Error fetching logs for ${containerId}:`, error);
  576. }
  577. },
  578. // 显示容器详情 (示例:用 SweetAlert 显示)
  579. async showContainerDetails(containerId) {
  580. core.showLoading('正在加载详情...');
  581. try {
  582. // 注意: 后端 /api/docker/containers/:id 需要存在并返回详细信息
  583. const response = await fetch(`/api/docker/containers/${containerId}`);
  584. if (!response.ok) {
  585. const errorData = await response.json().catch(() => ({ details: '无法解析错误响应' }));
  586. throw new Error(errorData.details || `获取详情失败 (${response.status})`);
  587. }
  588. const details = await response.json();
  589. core.hideLoading();
  590. // 格式化显示详情
  591. let detailsHtml = '<div class="container-details">';
  592. for (const key in details) {
  593. detailsHtml += `<p><strong>${key}:</strong> ${JSON.stringify(details[key], null, 2)}</p>`;
  594. }
  595. detailsHtml += '</div>';
  596. Swal.fire({
  597. title: `容器详情: ${details.Name || containerId.substring(0, 6)}`,
  598. html: detailsHtml,
  599. width: '80%',
  600. confirmButtonText: '关闭'
  601. });
  602. } catch (error) {
  603. core.hideLoading();
  604. core.showAlert(`查看详情失败: ${error.message}`, 'error');
  605. logger.error(`[dockerManager] Error fetching details for ${containerId}:`, error);
  606. }
  607. },
  608. // 启动容器
  609. async startContainer(containerId) {
  610. core.showLoading('正在启动容器...');
  611. try {
  612. const response = await fetch(`/api/docker/containers/${containerId}/start`, { method: 'POST' });
  613. const data = await response.json();
  614. core.hideLoading();
  615. if (!response.ok) throw new Error(data.details || '启动容器失败');
  616. core.showAlert('容器启动成功', 'success');
  617. systemStatus.refreshSystemStatus(); // 刷新整体状态
  618. } catch (error) {
  619. core.hideLoading();
  620. core.showAlert(`启动容器失败: ${error.message}`, 'error');
  621. logger.error(`[dockerManager] Error starting container ${containerId}:`, error);
  622. }
  623. },
  624. // 停止容器
  625. async stopContainer(containerId) {
  626. core.showLoading('正在停止容器...');
  627. try {
  628. const response = await fetch(`/api/docker/containers/${containerId}/stop`, { method: 'POST' });
  629. const data = await response.json();
  630. core.hideLoading();
  631. if (!response.ok && response.status !== 304) { // 304 Not Modified 也算成功(已停止)
  632. throw new Error(data.details || '停止容器失败');
  633. }
  634. core.showAlert(data.message || '容器停止成功', 'success');
  635. systemStatus.refreshSystemStatus(); // 刷新整体状态
  636. // 刷新已停止容器列表
  637. if (window.app && typeof window.app.refreshStoppedContainers === 'function') {
  638. window.app.refreshStoppedContainers();
  639. }
  640. } catch (error) {
  641. core.hideLoading();
  642. core.showAlert(`停止容器失败: ${error.message}`, 'error');
  643. logger.error(`[dockerManager] Error stopping container ${containerId}:`, error);
  644. }
  645. },
  646. // 重启容器
  647. async restartContainer(containerId) {
  648. core.showLoading('正在重启容器...');
  649. try {
  650. const response = await fetch(`/api/docker/containers/${containerId}/restart`, { method: 'POST' });
  651. const data = await response.json();
  652. core.hideLoading();
  653. if (!response.ok) throw new Error(data.details || '重启容器失败');
  654. core.showAlert('容器重启成功', 'success');
  655. systemStatus.refreshSystemStatus(); // 刷新整体状态
  656. } catch (error) {
  657. core.hideLoading();
  658. core.showAlert(`重启容器失败: ${error.message}`, 'error');
  659. logger.error(`[dockerManager] Error restarting container ${containerId}:`, error);
  660. }
  661. },
  662. // 删除容器 (带确认)
  663. removeContainer(containerId) {
  664. Swal.fire({
  665. title: '确认删除?',
  666. text: `确定要删除容器 ${containerId.substring(0, 6)} 吗?此操作不可恢复!`,
  667. icon: 'warning',
  668. showCancelButton: true,
  669. confirmButtonColor: 'var(--danger-color)',
  670. cancelButtonColor: '#6c757d',
  671. confirmButtonText: '确认删除',
  672. cancelButtonText: '取消'
  673. }).then(async (result) => {
  674. if (result.isConfirmed) {
  675. core.showLoading('正在删除容器...');
  676. try {
  677. const response = await fetch(`/api/docker/containers/${containerId}/remove`, { method: 'POST' }); // 使用 remove
  678. const data = await response.json();
  679. core.hideLoading();
  680. if (!response.ok) throw new Error(data.details || '删除容器失败');
  681. core.showAlert(data.message || '容器删除成功', 'success');
  682. systemStatus.refreshSystemStatus(); // 刷新整体状态
  683. } catch (error) {
  684. core.hideLoading();
  685. core.showAlert(`删除容器失败: ${error.message}`, 'error');
  686. logger.error(`[dockerManager] Error removing container ${containerId}:`, error);
  687. }
  688. }
  689. });
  690. },
  691. // --- 新增:更新容器函数 ---
  692. async updateContainer(containerId, currentImage) {
  693. const imageName = currentImage.split(':')[0]; // 提取基础镜像名
  694. const { value: newTag } = await Swal.fire({
  695. title: `更新容器: ${imageName}`,
  696. input: 'text',
  697. inputLabel: '请输入新的镜像标签 (例如 latest, v1.2)',
  698. inputValue: 'latest', // 默认值
  699. showCancelButton: true,
  700. confirmButtonText: '开始更新',
  701. cancelButtonText: '取消',
  702. confirmButtonColor: '#3085d6',
  703. cancelButtonColor: '#d33',
  704. width: '36em', // 增加弹窗宽度
  705. inputValidator: (value) => {
  706. if (!value || value.trim() === '') {
  707. return '镜像标签不能为空!';
  708. }
  709. },
  710. // 美化弹窗样式
  711. customClass: {
  712. container: 'update-container',
  713. popup: 'update-popup',
  714. header: 'update-header',
  715. title: 'update-title',
  716. closeButton: 'update-close',
  717. icon: 'update-icon',
  718. image: 'update-image',
  719. content: 'update-content',
  720. input: 'update-input',
  721. actions: 'update-actions',
  722. confirmButton: 'update-confirm',
  723. cancelButton: 'update-cancel',
  724. footer: 'update-footer'
  725. },
  726. // 添加自定义CSS
  727. didOpen: () => {
  728. // 修复输入框宽度
  729. const inputElement = Swal.getInput();
  730. if (inputElement) {
  731. inputElement.style.maxWidth = '100%';
  732. inputElement.style.width = '100%';
  733. inputElement.style.boxSizing = 'border-box';
  734. inputElement.style.margin = '0';
  735. inputElement.style.padding = '0.5rem';
  736. }
  737. // 修复输入标签宽度
  738. const inputLabel = Swal.getPopup().querySelector('.swal2-input-label');
  739. if (inputLabel) {
  740. inputLabel.style.whiteSpace = 'normal';
  741. inputLabel.style.textAlign = 'left';
  742. inputLabel.style.width = '100%';
  743. inputLabel.style.padding = '0 10px';
  744. inputLabel.style.boxSizing = 'border-box';
  745. inputLabel.style.marginBottom = '0.5rem';
  746. }
  747. // 调整弹窗内容区域
  748. const content = Swal.getPopup().querySelector('.swal2-content');
  749. if (content) {
  750. content.style.padding = '0 1.5rem';
  751. content.style.boxSizing = 'border-box';
  752. content.style.width = '100%';
  753. }
  754. }
  755. });
  756. if (newTag) {
  757. // 显示进度弹窗
  758. Swal.fire({
  759. title: '更新容器',
  760. html: `
  761. <div class="update-progress">
  762. <p>正在更新容器 <strong>${containerId.substring(0, 8)}</strong></p>
  763. <p>镜像: <strong>${imageName}:${newTag.trim()}</strong></p>
  764. <div class="progress-status">准备中...</div>
  765. <div class="progress-container">
  766. <div class="progress-bar"></div>
  767. </div>
  768. </div>
  769. `,
  770. showConfirmButton: false,
  771. allowOutsideClick: false,
  772. allowEscapeKey: false,
  773. didOpen: () => {
  774. const progressBar = Swal.getPopup().querySelector('.progress-bar');
  775. const progressStatus = Swal.getPopup().querySelector('.progress-status');
  776. // 设置初始进度
  777. progressBar.style.width = '0%';
  778. progressBar.style.backgroundColor = '#4CAF50';
  779. // 模拟进度动画
  780. let progress = 0;
  781. const progressInterval = setInterval(() => {
  782. // 进度最多到95%,剩下的在请求完成后处理
  783. if (progress < 95) {
  784. progress += Math.random() * 3;
  785. if (progress > 95) progress = 95;
  786. progressBar.style.width = `${progress}%`;
  787. // 更新状态文本
  788. if (progress < 30) {
  789. progressStatus.textContent = "拉取新镜像...";
  790. } else if (progress < 60) {
  791. progressStatus.textContent = "准备更新容器...";
  792. } else if (progress < 90) {
  793. progressStatus.textContent = "应用新配置...";
  794. } else {
  795. progressStatus.textContent = "即将完成...";
  796. }
  797. }
  798. }, 300);
  799. // 发送更新请求
  800. this.performContainerUpdate(containerId, newTag.trim(), progressBar, progressStatus, progressInterval);
  801. }
  802. });
  803. }
  804. },
  805. // 执行容器更新请求
  806. async performContainerUpdate(containerId, newTag, progressBar, progressStatus, progressInterval) {
  807. try {
  808. const response = await fetch(`/api/docker/containers/${containerId}/update`, {
  809. method: 'POST',
  810. headers: {
  811. 'Content-Type': 'application/json'
  812. },
  813. body: JSON.stringify({ tag: newTag })
  814. });
  815. // 清除进度定时器
  816. clearInterval(progressInterval);
  817. if (response.ok) {
  818. const data = await response.json();
  819. // 设置进度为100%
  820. progressBar.style.width = '100%';
  821. progressStatus.textContent = "更新完成!";
  822. // 显示成功消息
  823. setTimeout(() => {
  824. Swal.fire({
  825. icon: 'success',
  826. title: '更新成功!',
  827. text: data.message || '容器已成功更新',
  828. confirmButtonText: '确定'
  829. });
  830. // 刷新容器列表
  831. systemStatus.refreshSystemStatus();
  832. }, 800);
  833. } else {
  834. const data = await response.json().catch(() => ({ error: '解析响应失败', details: '服务器返回了无效的数据' }));
  835. // 设置进度条为错误状态
  836. progressBar.style.width = '100%';
  837. progressBar.style.backgroundColor = '#f44336';
  838. progressStatus.textContent = "更新失败";
  839. // 显示错误消息
  840. setTimeout(() => {
  841. Swal.fire({
  842. icon: 'error',
  843. title: '更新失败',
  844. text: data.details || data.error || '未知错误',
  845. confirmButtonText: '确定'
  846. });
  847. }, 800);
  848. }
  849. } catch (error) {
  850. // 清除进度定时器
  851. clearInterval(progressInterval);
  852. // 设置进度条为错误状态
  853. progressBar.style.width = '100%';
  854. progressBar.style.backgroundColor = '#f44336';
  855. progressStatus.textContent = "更新出错";
  856. // 显示错误信息
  857. setTimeout(() => {
  858. Swal.fire({
  859. icon: 'error',
  860. title: '更新失败',
  861. text: error.message || '网络请求失败',
  862. confirmButtonText: '确定'
  863. });
  864. }, 800);
  865. // 记录错误日志
  866. logger.error(`[dockerManager] Error updating container ${containerId} to tag ${newTag}:`, error);
  867. }
  868. },
  869. // --- 新增:绑定排查按钮事件 ---
  870. bindTroubleshootButton() {
  871. // 使用 setTimeout 确保按钮已经渲染到 DOM 中
  872. setTimeout(() => {
  873. const troubleshootBtn = document.getElementById('docker-troubleshoot-btn');
  874. if (troubleshootBtn) {
  875. // 先移除旧监听器,防止重复绑定
  876. troubleshootBtn.replaceWith(troubleshootBtn.cloneNode(true));
  877. const newBtn = document.getElementById('docker-troubleshoot-btn'); // 重新获取克隆后的按钮
  878. if(newBtn) {
  879. newBtn.addEventListener('click', () => {
  880. if (window.systemStatus && typeof window.systemStatus.showDockerHelp === 'function') {
  881. window.systemStatus.showDockerHelp();
  882. } else {
  883. console.error('[dockerManager] systemStatus.showDockerHelp is not available.');
  884. // 可以提供一个备用提示
  885. alert('无法显示帮助信息,请检查控制台。');
  886. }
  887. });
  888. console.log('[dockerManager] Troubleshoot button event listener bound.');
  889. } else {
  890. console.warn('[dockerManager] Cloned troubleshoot button not found after replace.');
  891. }
  892. } else {
  893. console.warn('[dockerManager] Troubleshoot button not found for binding.');
  894. }
  895. }, 0); // 延迟 0ms 执行,让浏览器有机会渲染
  896. },
  897. // 新增方法: 应用表格样式
  898. applyTableStyles(table) {
  899. if (!table) return;
  900. // 添加基本样式
  901. table.style.width = "100%";
  902. table.style.tableLayout = "auto";
  903. table.style.borderCollapse = "collapse";
  904. // 设置表头样式
  905. const thead = table.querySelector('thead');
  906. if (thead) {
  907. thead.style.backgroundColor = "#f8f9fa";
  908. thead.style.fontWeight = "bold";
  909. const thCells = thead.querySelectorAll('th');
  910. thCells.forEach(th => {
  911. th.style.textAlign = "center";
  912. th.style.padding = "10px 8px";
  913. th.style.verticalAlign = "middle";
  914. });
  915. }
  916. // 添加响应式样式
  917. const style = document.createElement('style');
  918. style.textContent = `
  919. #dockerStatusTable {
  920. width: 100%;
  921. table-layout: auto;
  922. }
  923. #dockerStatusTable th, #dockerStatusTable td {
  924. text-align: center;
  925. vertical-align: middle;
  926. padding: 8px;
  927. }
  928. #dockerStatusTable td.action-cell {
  929. padding: 4px;
  930. }
  931. @media (max-width: 768px) {
  932. #dockerStatusTable {
  933. table-layout: fixed;
  934. }
  935. }
  936. `;
  937. // 检查是否已经添加了样式
  938. const existingStyle = document.querySelector('style[data-for="dockerStatusTable"]');
  939. if (!existingStyle) {
  940. style.setAttribute('data-for', 'dockerStatusTable');
  941. document.head.appendChild(style);
  942. }
  943. }
  944. };
  945. // 确保在 DOM 加载后初始化
  946. document.addEventListener('DOMContentLoaded', () => {
  947. // 注意:init 现在只设置监听器,不加载数据
  948. // dockerManager.init();
  949. // 可以在 app.js 或 systemStatus.js 初始化完成后调用
  950. });