1
0

systemStatus.js 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151
  1. /**
  2. * 系统状态管理模块
  3. */
  4. // --- 新增:用于缓存最新的系统状态数据 ---
  5. let currentSystemData = null;
  6. let DEBUG_MODE = false; // 控制是否输出调试信息
  7. // 简单的日志工具
  8. const logger = {
  9. debug: function(...args) {
  10. if (DEBUG_MODE) {
  11. // console.log('[systemStatus:DEBUG]', ...args);
  12. }
  13. },
  14. log: function(...args) {
  15. // console.log('[systemStatus]', ...args);
  16. },
  17. warn: function(...args) {
  18. // console.warn('[systemStatus]', ...args);
  19. },
  20. error: function(...args) {
  21. // console.error('[systemStatus]', ...args);
  22. },
  23. // 开启或关闭调试模式
  24. setDebug: function(enabled) {
  25. DEBUG_MODE = !!enabled;
  26. // console.log(`[systemStatus] 调试模式已${DEBUG_MODE ? '开启' : '关闭'}`);
  27. }
  28. };
  29. // 刷新系统状态
  30. async function refreshSystemStatus() {
  31. // logger.log('刷新系统状态...');
  32. showSystemStatusLoading(); // 显示表格/活动列表的加载状态
  33. showDashboardLoading(); // 显示仪表盘卡片的加载状态
  34. try {
  35. // 并行获取Docker状态和系统资源信息
  36. // logger.log('获取Docker状态和系统资源信息');
  37. const [dockerResponse, resourcesResponse] = await Promise.all([
  38. fetch('/api/docker/status').catch(err => { /* logger.error('Docker status fetch failed:', err); */ return null; }), // 添加 catch
  39. fetch('/api/system-resources').catch(err => { /* logger.error('System resources fetch failed:', err); */ return null; }) // 添加 catch
  40. ]);
  41. // logger.debug('API响应结果:', { dockerOk: dockerResponse?.ok, resourcesOk: resourcesResponse?.ok });
  42. let dockerDataArray = null; // 用于存放容器数组
  43. let isDockerServiceRunning = false; // 用于判断 Docker 服务本身是否响应
  44. if (dockerResponse && dockerResponse.ok) {
  45. try {
  46. // 假设 API 直接返回容器数组
  47. dockerDataArray = await dockerResponse.json();
  48. // logger.debug('Docker数据:', JSON.stringify(dockerDataArray));
  49. // 只有当返回的是数组,且状态属性表明 Docker 正在运行时,才认为 Docker 服务是运行的
  50. if (Array.isArray(dockerDataArray)) {
  51. // 检查特殊错误标记,这可能会在 dockerService.js 中添加
  52. const hasDockerUnavailableError = dockerDataArray.length === 1 &&
  53. dockerDataArray[0] &&
  54. dockerDataArray[0].error === 'DOCKER_UNAVAILABLE';
  55. const hasContainerListError = dockerDataArray.length === 1 &&
  56. dockerDataArray[0] &&
  57. dockerDataArray[0].error === 'CONTAINER_LIST_ERROR';
  58. // 只有在没有这两种特定错误时,才认为 Docker 服务正常
  59. isDockerServiceRunning = !hasDockerUnavailableError && !hasContainerListError;
  60. // logger.debug(`Docker服务状态: ${isDockerServiceRunning ? '运行中' : '未运行'}, 错误状态:`,
  61. // { hasDockerUnavailableError, hasContainerListError });
  62. } else {
  63. // logger.warn('Docker数据不是数组:', typeof dockerDataArray);
  64. isDockerServiceRunning = false;
  65. }
  66. } catch (jsonError) {
  67. // logger.error('解析Docker数据失败:', jsonError);
  68. dockerDataArray = []; // 解析失败视为空数组
  69. isDockerServiceRunning = false; // JSON 解析失败,认为服务有问题
  70. }
  71. } else {
  72. // logger.warn('获取Docker状态失败');
  73. dockerDataArray = []; // 请求失败视为空数组
  74. isDockerServiceRunning = false; // 请求失败,认为服务未运行
  75. }
  76. let resourcesData = null;
  77. if (resourcesResponse && resourcesResponse.ok) {
  78. try {
  79. // --- 添加日志:打印原始响应文本 ---
  80. const resourcesText = await resourcesResponse.text();
  81. // logger.debug('原始系统资源响应:', resourcesText);
  82. resourcesData = JSON.parse(resourcesText); // 解析文本
  83. // logger.debug('解析后的系统资源数据:', resourcesData);
  84. } catch (jsonError) {
  85. // logger.error('解析系统资源数据失败:', jsonError);
  86. resourcesData = { cpu: null, memory: null, diskSpace: null };
  87. }
  88. } else {
  89. // logger.warn(`获取系统资源失败, 状态: ${resourcesResponse?.status}`);
  90. resourcesData = { cpu: null, memory: null, diskSpace: null };
  91. }
  92. // 合并数据
  93. const combinedData = {
  94. // 直接使用 isDockerServiceRunning 判断状态
  95. dockerStatus: isDockerServiceRunning ? 'running' : 'stopped',
  96. // 直接使用获取到的容器数组
  97. dockerContainers: Array.isArray(dockerDataArray) ? dockerDataArray : [],
  98. cpu: resourcesData?.cpu || { cores: 0, usage: undefined },
  99. memory: resourcesData?.memory || { total: 0, free: 0, used: 0, usedPercentage: undefined },
  100. disk: resourcesData?.disk || { size: '未知', used: '未知', available: '未知', percent: '未知' },
  101. diskSpace: resourcesData?.diskSpace || { total: 0, free: 0, used: 0, usedPercentage: undefined },
  102. // recentActivities 的逻辑保持不变,如果需要从 docker 数据生成,需要调整
  103. // 暂时假设 recentActivities 来源于其他地方或保持为空
  104. recentActivities: [] // 确保是空数组,除非有其他来源
  105. };
  106. // --- 修改:将合并后的数据存入缓存 ---
  107. currentSystemData = combinedData; // 缓存数据
  108. // logger.debug('合并后的状态数据:', currentSystemData);
  109. updateSystemStatusUI(currentSystemData);
  110. // logger.log('系统状态加载完成');
  111. // 只在仪表盘页面(且登录框未显示时)才显示成功通知
  112. const adminContainer = document.querySelector('.admin-container');
  113. const loginModal = document.getElementById('loginModal');
  114. const isDashboardVisible = adminContainer && window.getComputedStyle(adminContainer).display !== 'none';
  115. const isLoginHidden = !loginModal || window.getComputedStyle(loginModal).display === 'none';
  116. if (isDashboardVisible && isLoginHidden) {
  117. // 显示成功通知
  118. Swal.fire({
  119. icon: 'success',
  120. title: '刷新成功',
  121. text: '系统状态信息已更新',
  122. toast: true,
  123. position: 'top-end',
  124. showConfirmButton: false,
  125. timer: 3000
  126. });
  127. }
  128. // 刷新已停止容器列表
  129. if (window.app && typeof window.app.refreshStoppedContainers === 'function') {
  130. window.app.refreshStoppedContainers();
  131. }
  132. } catch (error) {
  133. // logger.error('刷新系统状态出错:', error);
  134. showSystemStatusError(error.message);
  135. showDashboardError(error.message);
  136. // 只在仪表盘页面(且登录框未显示时)才显示错误通知
  137. const adminContainer = document.querySelector('.admin-container');
  138. const loginModal = document.getElementById('loginModal');
  139. const isDashboardVisible = adminContainer && window.getComputedStyle(adminContainer).display !== 'none';
  140. const isLoginHidden = !loginModal || window.getComputedStyle(loginModal).display === 'none';
  141. if (isDashboardVisible && isLoginHidden) {
  142. // 显示错误通知
  143. Swal.fire({
  144. icon: 'error',
  145. title: '刷新失败',
  146. text: error.message,
  147. toast: true,
  148. position: 'top-end',
  149. showConfirmButton: false,
  150. timer: 5000
  151. });
  152. }
  153. }
  154. }
  155. // 显示系统状态错误
  156. function showSystemStatusError(message) {
  157. // console.error('系统状态错误:', message);
  158. // 更新Docker容器表格状态为错误
  159. const containerBody = document.getElementById('dockerContainersBody');
  160. if (containerBody) {
  161. containerBody.innerHTML = `
  162. <tr>
  163. <td colspan="5" class="text-center">
  164. <div class="error-container">
  165. <i class="fas fa-exclamation-triangle text-danger fa-2x"></i>
  166. <p class="mt-2">无法加载Docker容器信息</p>
  167. <small class="text-muted">${message}</small>
  168. </div>
  169. </td>
  170. </tr>
  171. `;
  172. }
  173. // 更新活动表格状态为错误
  174. const activitiesTable = document.getElementById('recentActivitiesTable');
  175. if (activitiesTable) {
  176. const tbody = activitiesTable.querySelector('tbody') || activitiesTable;
  177. if (tbody) {
  178. tbody.innerHTML = `
  179. <tr>
  180. <td colspan="3" class="text-center">
  181. <div class="error-container">
  182. <i class="fas fa-exclamation-triangle text-danger fa-2x"></i>
  183. <p class="mt-2">无法加载系统活动信息</p>
  184. <small class="text-muted">${message}</small>
  185. </div>
  186. </td>
  187. </tr>
  188. `;
  189. }
  190. }
  191. }
  192. // 更新系统状态UI
  193. function updateSystemStatusUI(data) {
  194. // 移除详细的数据日志
  195. // console.log('[systemStatus] Updating UI with data:', data);
  196. if (!data) {
  197. showSystemStatusError('无法获取系统状态数据');
  198. showDashboardError('无法获取系统状态数据');
  199. return;
  200. }
  201. // 更新Docker状态指示器
  202. updateDockerStatus(data.dockerStatus === 'running');
  203. // 更新仪表盘卡片
  204. updateDashboardCards(data);
  205. // 更新活动列表
  206. updateActivitiesTable(Array.isArray(data.recentActivities) ? data.recentActivities : []);
  207. // --- 调用 dockerManager 来渲染容器表格 (确保调用) ---
  208. if (typeof dockerManager !== 'undefined' && dockerManager.renderContainersTable) {
  209. dockerManager.renderContainersTable(data.dockerContainers, data.dockerStatus);
  210. } else {
  211. // 如果 dockerManager 不可用,提供一个简单错误信息
  212. const containerBody = document.getElementById('dockerStatusTableBody');
  213. if (containerBody) {
  214. containerBody.innerHTML = `
  215. <tr>
  216. <td colspan="5" class="text-center text-danger">
  217. <i class="fas fa-exclamation-triangle me-2"></i> 无法加载容器管理组件
  218. </td>
  219. </tr>
  220. `;
  221. }
  222. }
  223. // 更新存储使用进度条
  224. try {
  225. // 使用可选链和确保 parseDiskSpace 返回有效对象
  226. const diskData = parseDiskSpace(data.diskSpace);
  227. // console.log('[systemStatus] Parsed disk data for progress bar:', diskData);
  228. if (diskData && typeof diskData.usagePercent === 'number') {
  229. updateProgressBar('diskSpaceProgress', diskData.usagePercent);
  230. // console.log(`[systemStatus] Updated disk progress bar with: ${diskData.usagePercent}%`);
  231. } else {
  232. updateProgressBar('diskSpaceProgress', 0); // 出错或无数据时归零
  233. // console.log('[systemStatus] Disk data invalid or missing usagePercent for progress bar.');
  234. }
  235. } catch (e) {
  236. // console.warn('[systemStatus] Failed to update disk progress bar:', e);
  237. updateProgressBar('diskSpaceProgress', 0); // 出错时归零
  238. }
  239. // 更新内存使用进度条
  240. try {
  241. // 使用可选链
  242. const memPercent = data.memory?.usedPercentage;
  243. if (typeof memPercent === 'number') {
  244. updateProgressBar('memoryProgress', memPercent);
  245. // console.log(`[systemStatus] Updated memory progress bar with: ${memPercent}%`);
  246. } else {
  247. updateProgressBar('memoryProgress', 0);
  248. // console.log('[systemStatus] Memory data invalid or missing usedPercentage for progress bar.');
  249. }
  250. } catch (e) {
  251. // console.warn('[systemStatus] Failed to update memory progress bar:', e);
  252. updateProgressBar('memoryProgress', 0);
  253. }
  254. // 更新CPU使用进度条
  255. try {
  256. // 使用可选链
  257. const cpuUsage = data.cpu?.usage;
  258. if (typeof cpuUsage === 'number') {
  259. updateProgressBar('cpuProgress', cpuUsage);
  260. // console.log(`[systemStatus] Updated CPU progress bar with: ${cpuUsage}%`);
  261. } else {
  262. updateProgressBar('cpuProgress', 0);
  263. // console.log('[systemStatus] CPU data invalid or missing usage for progress bar.');
  264. }
  265. } catch (e) {
  266. // console.warn('[systemStatus] Failed to update CPU progress bar:', e);
  267. updateProgressBar('cpuProgress', 0);
  268. }
  269. // console.log('[systemStatus] UI update process finished.');
  270. }
  271. // 显示系统状态加载
  272. function showSystemStatusLoading() {
  273. // console.log('显示系统状态加载中...');
  274. // 更新Docker容器表格状态为加载中
  275. const containerTable = document.getElementById('dockerContainersTable');
  276. const containerBody = document.getElementById('dockerContainersBody');
  277. if (containerTable && containerBody) {
  278. containerBody.innerHTML = `
  279. <tr>
  280. <td colspan="5" class="text-center">
  281. <div class="loading-container">
  282. <div class="spinner-border text-primary" role="status">
  283. <span class="visually-hidden">加载中...</span>
  284. </div>
  285. <p class="mt-2">正在加载Docker容器信息...</p>
  286. </div>
  287. </td>
  288. </tr>
  289. `;
  290. }
  291. // 更新活动表格状态为加载中
  292. const activitiesTable = document.getElementById('recentActivitiesTable');
  293. if (activitiesTable) {
  294. const tbody = activitiesTable.querySelector('tbody') || activitiesTable;
  295. if (tbody) {
  296. tbody.innerHTML = `
  297. <tr>
  298. <td colspan="3" class="text-center">
  299. <div class="loading-container">
  300. <div class="spinner-border text-primary" role="status">
  301. <span class="visually-hidden">加载中...</span>
  302. </div>
  303. <p class="mt-2">正在加载系统活动信息...</p>
  304. </div>
  305. </td>
  306. </tr>
  307. `;
  308. }
  309. }
  310. }
  311. // 更新进度条
  312. function updateProgressBar(id, percentage) {
  313. const bar = document.getElementById(id);
  314. if (bar) {
  315. // 确保 percentage 是有效的数字,否则设为 0
  316. const validPercentage = (typeof percentage === 'number' && !isNaN(percentage)) ? Math.max(0, Math.min(100, percentage)) : 0;
  317. // console.log(`[systemStatus] Setting progress bar ${id} to ${validPercentage}%`);
  318. bar.style.width = `${validPercentage}%`;
  319. bar.setAttribute('aria-valuenow', validPercentage); // 更新 aria 属性
  320. // 根据使用率改变颜色
  321. if (validPercentage < 50) {
  322. bar.className = 'progress-bar bg-success';
  323. } else if (validPercentage < 80) {
  324. bar.className = 'progress-bar bg-warning';
  325. } else {
  326. bar.className = 'progress-bar bg-danger';
  327. }
  328. } else {
  329. // console.warn(`[systemStatus] Progress bar element with id '${id}' not found.`);
  330. }
  331. }
  332. // 显示详情对话框 - 修改为直接使用缓存数据
  333. function showDetailsDialog(type) {
  334. try {
  335. let title = '';
  336. let htmlContent = ''; // 用于生成内容的变量
  337. if (!currentSystemData) {
  338. htmlContent = '<div class="error-message">系统状态数据尚未加载。</div>';
  339. } else {
  340. switch(type) {
  341. case 'containers':
  342. title = '容器详情';
  343. htmlContent = generateContainerDetailsHtml(currentSystemData.dockerContainers);
  344. break;
  345. case 'memory':
  346. title = '内存使用详情';
  347. htmlContent = generateMemoryDetailsHtml(currentSystemData.memory);
  348. break;
  349. case 'cpu':
  350. title = 'CPU 使用详情';
  351. htmlContent = generateCpuDetailsHtml(currentSystemData.cpu);
  352. break;
  353. case 'disk':
  354. title = '磁盘使用详情';
  355. htmlContent = generateDiskDetailsHtml(currentSystemData.disk);
  356. break;
  357. default:
  358. title = '详情';
  359. htmlContent = '<p>未知详情类型</p>';
  360. }
  361. }
  362. // 使用 SweetAlert2 显示对话框
  363. Swal.fire({
  364. title: title,
  365. html: `<div id="detailsContent" class="details-swal-content">${htmlContent}</div>`,
  366. width: '80%', // 可以更宽以容纳表格或详细信息
  367. showConfirmButton: false,
  368. showCloseButton: true,
  369. customClass: {
  370. popup: 'details-swal-popup' // 添加自定义类
  371. }
  372. });
  373. } catch (error) {
  374. // console.error('显示详情对话框失败:', error);
  375. core.showAlert('加载详情时出错: ' + error.message, 'error');
  376. }
  377. }
  378. // --- 修改:生成容器详情 HTML (类Excel表头+多行数据) ---
  379. function generateContainerDetailsHtml(containers) {
  380. if (!Array.isArray(containers)) return '<p class="text-muted text-center py-3">容器数据无效</p>';
  381. let html = '<div class="details-table-container">';
  382. // 添加 resource-details-excel 类并使用 table-bordered
  383. html += '<table class="table table-sm table-bordered table-hover details-table resource-details-excel">';
  384. html += '<thead class="table-light"><tr><th>ID</th><th>名称</th><th>镜像</th><th>状态</th><th>创建时间</th></tr></thead>';
  385. html += '<tbody>';
  386. if (containers.length > 0) {
  387. containers.forEach(container => {
  388. const statusClass = dockerManager.getContainerStatusClass(container.state);
  389. const containerId = container.id || '-';
  390. const containerName = container.name || '-';
  391. const containerImage = container.image || '-';
  392. const containerState = container.state || '-';
  393. const containerCreated = container.created ? formatTime(container.created) : '-';
  394. // 生成数据行,<td> 对应表头的顺序
  395. html += `<tr>
  396. <td title="${containerId}">${containerId.substring(0, 12)}</td>
  397. <td title="${containerName}">${containerName}</td>
  398. <td title="${containerImage}">${containerImage}</td>
  399. <td><span class="badge ${statusClass}">${containerState}</span></td>
  400. <td>${containerCreated}</td>
  401. </tr>`;
  402. });
  403. } else {
  404. html += '<tr><td colspan="5" class="text-center text-muted py-3">没有找到容器</td></tr>'; // Colspan 调整为 5
  405. }
  406. html += '</tbody></table></div>';
  407. return html;
  408. }
  409. // --- 修改:生成内存详情 HTML (类Excel表头+单行数据) ---
  410. function generateMemoryDetailsHtml(memoryData) {
  411. if (!memoryData) return '<p class="text-muted text-center py-3">内存数据不可用</p>';
  412. const total = formatByteSize(memoryData.total);
  413. const used = formatByteSize(memoryData.used);
  414. const free = formatByteSize(memoryData.free);
  415. let usagePercent = '未知';
  416. if (typeof memoryData.percent === 'string') {
  417. usagePercent = memoryData.percent;
  418. } else if (typeof memoryData.total === 'number' && typeof memoryData.used === 'number' && memoryData.total > 0) {
  419. const percent = (memoryData.used / memoryData.total) * 100;
  420. usagePercent = `${percent.toFixed(1)}%`;
  421. }
  422. let html = '<div class="details-table-container">';
  423. html += '<table class="table table-sm table-bordered details-table resource-details-excel">'; // 添加新类
  424. html += '<thead class="table-light"><tr><th>总内存</th><th>已用内存</th><th>空闲内存</th><th>内存使用率</th></tr></thead>';
  425. html += `<tbody id="memory-details-body">`;
  426. html += `<tr><td>${total}</td><td>${used}</td><td>${free}</td><td>${usagePercent}</td></tr>`;
  427. html += '</tbody></table></div>';
  428. return html;
  429. }
  430. // --- 修改:生成 CPU 详情 HTML (类Excel表头+单行数据) ---
  431. function generateCpuDetailsHtml(cpuData) {
  432. if (!cpuData) return '<p class="text-muted text-center py-3">CPU 数据不可用</p>';
  433. let usagePercent = '未知';
  434. if (Array.isArray(cpuData.loadAvg) && cpuData.loadAvg.length > 0) {
  435. const cores = cpuData.cores || 1;
  436. const cpuUsage = (cpuData.loadAvg[0] / cores) * 100;
  437. usagePercent = `${Math.min(cpuUsage, 100).toFixed(1)}%`;
  438. }
  439. // 处理CPU速度
  440. let cpuSpeed = '未知';
  441. if (cpuData.speed) {
  442. cpuSpeed = `${cpuData.speed} MHz`;
  443. }
  444. let html = '<div class="details-table-container">';
  445. html += '<table class="table table-sm table-bordered details-table resource-details-excel">'; // 添加新类
  446. html += '<thead class="table-light"><tr><th>CPU 核心数</th><th>CPU 型号</th><th>CPU 速度</th><th>当前使用率</th></tr></thead>';
  447. html += `<tbody id="cpu-details-body">`;
  448. html += `<tr><td>${cpuData.cores || '未知'}</td><td>${cpuData.model || '未知'}</td><td>${cpuSpeed}</td><td>${usagePercent}</td></tr>`;
  449. html += '</tbody></table></div>';
  450. // 如果有loadAvg,添加额外的负载信息表格
  451. if (Array.isArray(cpuData.loadAvg) && cpuData.loadAvg.length >= 3) {
  452. html += '<h6 class="mt-3">系统平均负载</h6>';
  453. html += '<table class="table table-sm table-bordered details-table">';
  454. html += '<thead class="table-light"><tr><th>1分钟</th><th>5分钟</th><th>15分钟</th></tr></thead>';
  455. html += '<tbody>';
  456. html += `<tr><td>${cpuData.loadAvg[0].toFixed(2)}</td><td>${cpuData.loadAvg[1].toFixed(2)}</td><td>${cpuData.loadAvg[2].toFixed(2)}</td></tr>`;
  457. html += '</tbody></table>';
  458. }
  459. return html;
  460. }
  461. // --- 修改:生成磁盘详情 HTML (类Excel表头+单行数据) ---
  462. function generateDiskDetailsHtml(diskData) {
  463. if (!diskData) return '<p class="text-muted text-center py-3">磁盘数据不可用</p>';
  464. // 确保有展示数据,无论是来自哪个来源
  465. if (currentSystemData && currentSystemData.disk && !diskData.size) {
  466. diskData = currentSystemData.disk;
  467. }
  468. let html = '<div class="details-table-container">';
  469. html += '<table class="table table-sm table-bordered details-table resource-details-excel">'; // 添加新类
  470. html += '<thead class="table-light"><tr><th>总空间</th><th>已用空间</th><th>可用空间</th><th>使用率</th></tr></thead>';
  471. html += `<tbody id="disk-details-body">`;
  472. html += `<tr><td>${diskData.size || '未知'}</td><td>${diskData.used || '未知'}</td><td>${diskData.available || '未知'}</td><td>${diskData.percent || '未知'}</td></tr>`;
  473. html += '</tbody></table></div>';
  474. // 添加额外的文件系统信息表格
  475. if (diskData.filesystem) {
  476. html += '<h6 class="mt-3">文件系统信息</h6>';
  477. html += '<table class="table table-sm table-bordered details-table">';
  478. html += '<tbody>';
  479. html += `<tr><th>文件系统</th><td>${diskData.filesystem}</td></tr>`;
  480. if (diskData.mounted) {
  481. html += `<tr><th>挂载点</th><td>${diskData.mounted}</td></tr>`;
  482. }
  483. html += '</tbody></table>';
  484. }
  485. return html;
  486. }
  487. // 更新Docker状态指示器
  488. function updateDockerStatus(available) {
  489. // console.log(`[systemStatus] Updating top Docker status indicator to: ${available ? 'running' : 'stopped'}`);
  490. const statusIndicator = document.getElementById('dockerStatusIndicator'); // 假设这是顶部指示器的 ID
  491. const statusText = document.getElementById('dockerStatusText'); // 假设这是顶部文本的 ID
  492. if (!statusIndicator || !statusText) {
  493. console.warn('[systemStatus] Top Docker status indicator elements not found.');
  494. return;
  495. }
  496. if (available) {
  497. statusIndicator.style.backgroundColor = 'var(--success-color, #4CAF50)'; // 使用 CSS 变量或默认值
  498. statusText.textContent = 'Docker 运行中';
  499. statusIndicator.title = 'Docker 服务正常运行中';
  500. statusIndicator.classList.remove('stopped');
  501. statusIndicator.classList.add('running');
  502. } else {
  503. statusIndicator.style.backgroundColor = 'var(--danger-color, #f44336)';
  504. statusText.textContent = 'Docker 未运行';
  505. statusIndicator.title = 'Docker 服务未运行或无法访问';
  506. statusIndicator.classList.remove('running');
  507. statusIndicator.classList.add('stopped');
  508. }
  509. }
  510. // 显示Docker帮助信息
  511. function showDockerHelp() {
  512. Swal.fire({
  513. title: '<i class="fas fa-info-circle"></i> Docker 服务未运行',
  514. html: `
  515. <div class="docker-help-content text-start" style="text-align: left;">
  516. <p class="mb-3">看起来 Docker 服务当前没有运行,或者应用程序无法连接到它。请检查以下常见原因:</p>
  517. <ol class="mb-4" style="padding-left: 20px;">
  518. <li class="mb-2"><strong>服务未启动:</strong> 确保 Docker Desktop (Windows/Mac) 或 Docker daemon (Linux) 正在运行。</li>
  519. <li class="mb-2"><strong>权限问题:</strong> 运行此程序的用户可能需要添加到 'docker' 用户组 (Linux)。</li>
  520. <li class="mb-2"><strong>Docker Socket:</strong> 确认 Docker Socket 文件的路径和权限是否正确配置 (通常是 <code>/var/run/docker.sock</code> on Linux)。</li>
  521. <li class="mb-2"><strong>防火墙:</strong> 检查是否有防火墙规则阻止了与 Docker 的通信。</li>
  522. </ol>
  523. <div class="docker-cmd-examples">
  524. <p><strong>常用诊断命令 (Linux):</strong></p>
  525. <pre class="code-block"><code>sudo systemctl status docker</code></pre>
  526. <pre class="code-block"><code>sudo systemctl start docker</code></pre>
  527. <pre class="code-block"><code>sudo systemctl enable docker</code></pre>
  528. <pre class="code-block"><code>docker ps</code></pre>
  529. <pre class="code-block"><code>groups ${USER} # 检查是否在 docker 组</code></pre>
  530. <pre class="code-block mb-0"><code>sudo usermod -aG docker ${USER} # 添加用户到 docker 组 (需要重新登录生效)</code></pre>
  531. </div>
  532. <p class="mt-3 small text-muted">如果问题仍然存在,请查阅 Docker 官方文档或检查应用程序的日志。</p>
  533. </div>
  534. `,
  535. icon: null,
  536. confirmButtonText: '我知道了',
  537. customClass: {
  538. popup: 'docker-help-popup',
  539. content: 'docker-help-swal-content'
  540. },
  541. width: '650px'
  542. });
  543. }
  544. // 初始化仪表板
  545. function initDashboard() {
  546. // 检查仪表板容器是否存在
  547. const dashboardGrid = document.querySelector('.dashboard-grid');
  548. if (!dashboardGrid) return;
  549. // 清空现有内容
  550. dashboardGrid.innerHTML = '';
  551. // 添加四个统计卡片
  552. const cardsData = [
  553. {
  554. id: 'containers',
  555. title: '容器数量',
  556. icon: 'fa-cubes',
  557. value: '--',
  558. description: '运行中的容器总数',
  559. trend: '',
  560. action: '查看详情'
  561. },
  562. {
  563. id: 'memory',
  564. title: '内存使用',
  565. icon: 'fa-memory',
  566. value: '--',
  567. description: '内存占用百分比',
  568. trend: '',
  569. action: '查看详情'
  570. },
  571. {
  572. id: 'cpu',
  573. title: 'CPU负载',
  574. icon: 'fa-microchip',
  575. value: '--',
  576. description: 'CPU平均负载',
  577. trend: '',
  578. action: '查看详情'
  579. },
  580. {
  581. id: 'disk',
  582. title: '磁盘空间',
  583. icon: 'fa-hdd',
  584. value: '--',
  585. description: '磁盘占用百分比',
  586. trend: '',
  587. action: '查看详情'
  588. }
  589. ];
  590. // 为每个卡片创建HTML并添加到仪表板网格
  591. cardsData.forEach(card => {
  592. const cardElement = createDashboardCard(card);
  593. dashboardGrid.appendChild(cardElement);
  594. });
  595. // 初始化刷新按钮
  596. const refreshBtn = document.getElementById('refreshSystemBtn');
  597. if (refreshBtn) {
  598. refreshBtn.addEventListener('click', refreshSystemStatus);
  599. }
  600. // 初始加载系统状态
  601. refreshSystemStatus(); // <-- 调用确保加载数据
  602. // console.log('仪表板初始化完成');
  603. }
  604. // 创建仪表板卡片
  605. function createDashboardCard(cardInfo) {
  606. const card = document.createElement('div');
  607. card.className = 'dashboard-card';
  608. card.id = `${cardInfo.id}-card`;
  609. // Icon: Use cardInfo.icon directly as it should contain the full class string (e.g., "fas fa-microchip")
  610. const iconHtml = cardInfo.icon ? `<i class="${cardInfo.icon}"></i>` : '';
  611. // Action: Determine if it's a link or a callback
  612. let actionHtml = '';
  613. if (cardInfo.actionLink) {
  614. actionHtml = `<a href="${cardInfo.actionLink}" class="card-action">${cardInfo.actionText || '查看'}</a>`;
  615. } else if (cardInfo.actionCallback && typeof cardInfo.actionCallback === 'function') {
  616. // Register the callback and create a link to call handleCardAction
  617. window.systemStatus.cardActionCallbacks[cardInfo.id] = cardInfo.actionCallback;
  618. actionHtml = `<a href="javascript:void(0)" class="card-action" onclick="window.systemStatus.handleCardAction('${cardInfo.id}')">${cardInfo.actionText || '操作'}</a>`;
  619. } else if (cardInfo.actionText) {
  620. // Fallback if only actionText is provided (no specific action)
  621. actionHtml = `<span class="card-action is-text">${cardInfo.actionText}</span>`;
  622. }
  623. card.innerHTML = `
  624. <div class="card-icon" style="background-color: ${cardInfo.color || 'var(--primary-light)'}; color: ${cardInfo.color ? 'white' : 'var(--primary-color)'};">
  625. ${iconHtml}
  626. </div>
  627. <div class="card-content">
  628. <h3 class="card-title">${cardInfo.title || '未命名卡片'}</h3>
  629. <div class="card-value ${cardInfo.valueClass || ''}" id="${cardInfo.id}-value">
  630. ${cardInfo.value !== undefined ? cardInfo.value : '--'} ${cardInfo.unit || ''}
  631. </div>
  632. ${cardInfo.description ? `<div class="card-description">${cardInfo.description}</div>` : ''}
  633. </div>
  634. ${actionHtml ? `<div class="card-footer">${actionHtml}</div>` : ''}
  635. `;
  636. return card;
  637. }
  638. // 更新仪表板卡片
  639. function updateDashboardCards(data) {
  640. logger.debug('Updating dashboard cards with data:', data);
  641. const dashboardGrid = document.querySelector('.dashboard-grid');
  642. if (!dashboardGrid) {
  643. logger.warn('Dashboard grid not found. Cannot update cards.');
  644. return;
  645. }
  646. // 清空旧的回调
  647. window.systemStatus.cardActionCallbacks = {};
  648. // 确保有 dockerContainers 数据
  649. const containers = Array.isArray(data.dockerContainers) ? data.dockerContainers : [];
  650. // 过滤掉错误指示对象和没有有效ID的容器
  651. const validContainers = containers.filter(c => c && c.id && c.id !== 'n/a' && !c.error);
  652. const runningContainersCount = validContainers.filter(c => c.state && typeof c.state === 'string' && c.state.toLowerCase().includes('running')).length;
  653. const totalValidContainersCount = validContainers.length;
  654. logger.debug('Valid containers for dashboard:', validContainers);
  655. logger.debug(`Running: ${runningContainersCount}, Total Valid: ${totalValidContainersCount}`);
  656. const cardsData = [
  657. {
  658. id: 'dockerStatus',
  659. title: 'Docker 服务',
  660. value: data.dockerStatus === 'running' ? '运行中' : '已停止',
  661. description: data.dockerStatus === 'running' ? '服务连接正常' : '服务未连接或不可用',
  662. icon: 'fab fa-docker',
  663. color: data.dockerStatus === 'running' ? 'var(--success-color)' : 'var(--danger-color)',
  664. actionText: data.dockerStatus === 'running' ? '查看容器' : null,
  665. actionLink: data.dockerStatus === 'running' ? '#docker-status' : null,
  666. valueClass: data.dockerStatus === 'running' ? 'text-success' : 'text-danger'
  667. },
  668. {
  669. id: 'containersCount',
  670. title: '运行中容器',
  671. value: runningContainersCount,
  672. description: `总容器数: ${totalValidContainersCount}`,
  673. icon: 'fas fa-box-open',
  674. color: 'var(--primary-color)',
  675. actionText: '管理容器',
  676. actionCallback: () => {
  677. if (typeof dockerManager !== 'undefined' && typeof dockerManager.showContainersPage === 'function') {
  678. core.navigateToSection('docker-status');
  679. } else if (typeof dockerManager !== 'undefined' && typeof dockerManager.showManagementModal === 'function') {
  680. dockerManager.showManagementModal();
  681. } else {
  682. window.location.hash = 'docker-status';
  683. if (window.app && typeof window.app.handleNavigation === 'function') {
  684. window.app.handleNavigation('docker-status');
  685. } else if (core && typeof core.navigateToSection === 'function') {
  686. core.navigateToSection('docker-status');
  687. } else {
  688. console.warn('No function found to display Docker management (modal or section).');
  689. const sidebarItem = document.querySelector('li[data-section="docker-status"]');
  690. if (sidebarItem) sidebarItem.click();
  691. else alert('容器管理功能导航失败');
  692. }
  693. }
  694. },
  695. unit: '个'
  696. },
  697. {
  698. id: 'cpuUsage',
  699. title: 'CPU 使用率',
  700. value: (() => {
  701. if (data.cpu) {
  702. if (typeof data.cpu.usage === 'number' && !isNaN(data.cpu.usage)) return data.cpu.usage.toFixed(1) + '%';
  703. if (typeof data.cpu.currentLoad === 'number' && !isNaN(data.cpu.currentLoad)) return data.cpu.currentLoad.toFixed(1) + '%';
  704. if (typeof data.cpu.load === 'number' && !isNaN(data.cpu.load)) return data.cpu.load.toFixed(1) + '%';
  705. if (data.cpu.currentLoad && typeof data.cpu.currentLoad.avgload === 'number' && !isNaN(data.cpu.currentLoad.avgload)) return data.cpu.currentLoad.avgload.toFixed(1) + '%';
  706. // Add logic to calculate from loadAvg, similar to generateCpuDetailsHtml
  707. if (Array.isArray(data.cpu.loadAvg) && data.cpu.loadAvg.length > 0 && typeof data.cpu.loadAvg[0] === 'number' && !isNaN(data.cpu.loadAvg[0])) {
  708. const cores = data.cpu.cores || 1;
  709. const cpuUsageFromLoadAvg = (data.cpu.loadAvg[0] / cores) * 100;
  710. return Math.min(cpuUsageFromLoadAvg, 100).toFixed(1) + '%';
  711. }
  712. }
  713. return 'N/A';
  714. })(),
  715. description: `核心数: ${data.cpu ? (data.cpu.cores || 'N/A') : 'N/A'}`,
  716. icon: 'fas fa-microchip',
  717. color: 'var(--warning-color)',
  718. actionText: '详细信息',
  719. actionCallback: () => showDetailsDialog('cpu'),
  720. unit: ''
  721. },
  722. {
  723. id: 'memoryUsage',
  724. title: '内存使用率',
  725. value: (() => {
  726. if (data.memory) {
  727. // 优先基于 active 内存计算百分比 (更接近真实程序占用)
  728. if (typeof data.memory.active === 'number' && typeof data.memory.total === 'number' && data.memory.total > 0 && !isNaN(data.memory.active)) {
  729. return ((data.memory.active / data.memory.total) * 100).toFixed(1) + '%';
  730. }
  731. // 如果后端直接提供了基于某种标准计算的 usedPercentage,且 active 不可用,则使用它
  732. if (typeof data.memory.usedPercentage === 'number' && !isNaN(data.memory.usedPercentage)) {
  733. return data.memory.usedPercentage.toFixed(1) + '%';
  734. }
  735. // 最后,如果 active 和 usedPercentage 都没有,才基于 used 计算 (这可能包含缓存)
  736. if (typeof data.memory.used === 'number' && typeof data.memory.total === 'number' && data.memory.total > 0 && !isNaN(data.memory.used)) {
  737. return ((data.memory.used / data.memory.total) * 100).toFixed(1) + '%';
  738. }
  739. }
  740. return 'N/A';
  741. })(),
  742. description: `已用: ${data.memory && typeof data.memory.used === 'number' && !isNaN(data.memory.used) ? formatByteSize(data.memory.used) : 'N/A'} / 总量: ${data.memory && data.memory.total ? formatByteSize(data.memory.total) : 'N/A'}`,
  743. icon: 'fas fa-memory',
  744. color: 'var(--info-color)',
  745. actionText: '详细信息',
  746. actionCallback: () => showDetailsDialog('memory'),
  747. unit: ''
  748. },
  749. {
  750. id: 'diskUsage',
  751. title: '磁盘使用率',
  752. value: data.disk && data.disk.percent ? data.disk.percent : 'N/A',
  753. description: `可用: ${data.disk && data.disk.available ? data.disk.available : 'N/A'} / 总量: ${data.disk && data.disk.size ? data.disk.size : 'N/A'}`,
  754. icon: 'fas fa-hdd',
  755. color: 'var(--secondary-color)',
  756. actionText: '详细信息',
  757. actionCallback: () => showDetailsDialog('disk'),
  758. unit: ''
  759. },
  760. {
  761. id: 'uptime', // 假设有一个API可以获取系统启动时间
  762. title: '系统运行时长',
  763. value: data.uptime || 'N/A', // 从 data.uptime 获取
  764. description: '系统持续运行时间',
  765. icon: 'fas fa-clock',
  766. color: 'var(--purple-color)', // 自定义颜色变量
  767. actionText: '系统日志',
  768. actionLink: '#system-logs', // 假设有系统日志页面
  769. unit: ''
  770. }
  771. ];
  772. dashboardGrid.innerHTML = ''; // 清空现有卡片
  773. cardsData.forEach(cardInfo => {
  774. if ((cardInfo.id === 'uptime' && !data.uptime) && cardInfo.id !== 'dockerStatus' && cardInfo.id !== 'containersCount') {
  775. // 如果是 uptime卡片且没有uptime数据,则跳过 (但保留 Docker 和容器数量卡片)
  776. if (cardInfo.id !== 'cpuUsage' && cardInfo.id !== 'memoryUsage' && cardInfo.id !== 'diskUsage') {
  777. logger.debug(`Skipping card ${cardInfo.id} due to missing data or specific condition.`);
  778. return;
  779. }
  780. }
  781. // 特殊处理 Docker 服务卡片,即使服务未运行也显示
  782. if (cardInfo.id === 'dockerStatus') {
  783. // 总是创建 Docker 状态卡片
  784. } else if (data.dockerStatus !== 'running' &&
  785. (cardInfo.id === 'containersCount')) {
  786. // 如果Docker服务未运行,则只显示 "N/A" 或 0 对于容器数量
  787. cardInfo.value = 0; // 或 'N/A'
  788. cardInfo.description = 'Docker 服务未运行';
  789. } else if (data.dockerStatus !== 'running' && cardInfo.id !== 'uptime') {
  790. // 如果 Docker 服务未运行,并且不是 uptime 卡片,则跳过其他依赖 Docker 的卡片
  791. if (cardInfo.id !== 'cpuUsage' && cardInfo.id !== 'memoryUsage' && cardInfo.id !== 'diskUsage') {
  792. logger.debug(`Skipping card ${cardInfo.id} because Docker is not running.`);
  793. return;
  794. }
  795. }
  796. const cardElement = createDashboardCard(cardInfo);
  797. dashboardGrid.appendChild(cardElement);
  798. });
  799. }
  800. // 格式化字节大小为易读格式
  801. function formatByteSize(bytes) {
  802. if (bytes === undefined || bytes === null) return '未知';
  803. if (typeof bytes === 'string') {
  804. if (!isNaN(parseInt(bytes))) {
  805. bytes = parseInt(bytes);
  806. } else {
  807. return bytes; // 如果已经是格式化字符串,直接返回
  808. }
  809. }
  810. if (typeof bytes !== 'number' || isNaN(bytes)) return '未知';
  811. const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
  812. let size = bytes;
  813. let unitIndex = 0;
  814. while (size >= 1024 && unitIndex < units.length - 1) {
  815. size /= 1024;
  816. unitIndex++;
  817. }
  818. return `${size.toFixed(2)} ${units[unitIndex]}`;
  819. }
  820. // 更新内存详情表格
  821. function updateMemoryDetailsTable(total, used, free, percent) {
  822. const memDetailsBody = document.getElementById('memory-details-body');
  823. if (memDetailsBody) {
  824. const percentText = typeof percent === 'number' ? `${percent.toFixed(1)}%` : '未知';
  825. memDetailsBody.innerHTML = `<tr><td>${total}</td><td>${used}</td><td>${free}</td><td>${percentText}</td></tr>`;
  826. }
  827. }
  828. // 更新CPU详情表格
  829. function updateCpuDetailsTable(cores, model, speed, usage) {
  830. const cpuDetailsBody = document.getElementById('cpu-details-body');
  831. if (cpuDetailsBody) {
  832. const usageText = typeof usage === 'number' ? `${usage.toFixed(1)}%` : '未知';
  833. let speedText = speed;
  834. if (typeof speed === 'number') {
  835. speedText = `${speed} MHz`;
  836. }
  837. cpuDetailsBody.innerHTML = `<tr><td>${cores || '未知'}</td><td>${model || '未知'}</td><td>${speedText}</td><td>${usageText}</td></tr>`;
  838. }
  839. }
  840. // 更新磁盘详情表格
  841. function updateDiskDetailsTable(total, used, available, percent) {
  842. const diskDetailsBody = document.getElementById('disk-details-body');
  843. if (diskDetailsBody) {
  844. const percentText = typeof percent === 'number' ? `${percent.toFixed(0)}%` : '未知';
  845. diskDetailsBody.innerHTML = `<tr><td>${total}</td><td>${used}</td><td>${available}</td><td>${percentText}</td></tr>`;
  846. }
  847. }
  848. // 更新活动表格
  849. function updateActivitiesTable(activities) {
  850. const table = document.getElementById('recentActivitiesTable');
  851. if (!table) {
  852. console.warn('[systemStatus] Recent activities table not found.');
  853. return;
  854. }
  855. // 获取 tbody,如果不存在则创建
  856. let tbody = table.querySelector('tbody');
  857. if (!tbody) {
  858. tbody = document.createElement('tbody');
  859. table.appendChild(tbody);
  860. }
  861. // 清空表格内容
  862. tbody.innerHTML = '';
  863. // 确保 activities 是数组
  864. if (!Array.isArray(activities)) {
  865. console.warn('[systemStatus] activities data is not an array:', activities);
  866. activities = []; // 设为空数组避免错误
  867. }
  868. if (activities.length === 0) {
  869. tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted py-3"><i class="fas fa-info-circle me-2"></i>暂无活动记录</td></tr>';
  870. } else {
  871. let html = '';
  872. activities.forEach(activity => {
  873. // 添加简单的 HTML 转义以防止 XSS (更健壮的方案应使用库)
  874. const safeDetails = (activity.details || '').replace(/</g, "&lt;").replace(/>/g, "&gt;");
  875. html += `
  876. <tr>
  877. <td data-label="时间">${formatTime(activity.timestamp)}</td>
  878. <td data-label="事件">${activity.event || '未知事件'}</td>
  879. <td data-label="详情" title="${safeDetails}" class="text-truncate">${safeDetails}</td>
  880. </tr>
  881. `;
  882. });
  883. tbody.innerHTML = html;
  884. }
  885. console.log(`[systemStatus] Updated activities table with ${activities.length} items.`);
  886. }
  887. // 格式化时间
  888. function formatTime(timestamp) {
  889. if (!timestamp) return '未知时间';
  890. const date = new Date(timestamp);
  891. const now = new Date();
  892. // 如果是今天的时间,只显示小时和分钟
  893. if (date.toDateString() === now.toDateString()) {
  894. return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
  895. }
  896. // 否则显示完整日期和时间
  897. return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
  898. }
  899. // 显示仪表盘加载状态
  900. function showDashboardLoading() {
  901. const dashboardGrid = document.querySelector('.dashboard-grid');
  902. if (dashboardGrid) {
  903. // Instead of targeting specific value elements, show a general loading message
  904. // or a spinner within the grid itself if it's empty.
  905. // For now, we'll rely on the card creation process to populate it.
  906. // If cards are not yet defined, we can show a message.
  907. if (dashboardGrid.children.length === 0) {
  908. dashboardGrid.innerHTML = '<div class="loading-message"><div class="loading-spinner-large"></div><p>正在加载控制面板数据...</p></div>';
  909. }
  910. }
  911. // The old method below targets specific value elements which might not exist before cards are drawn.
  912. // const cards = ['dockerStatus', 'containersCount', 'cpuUsage', 'memoryUsage', 'diskUsage', 'uptime'];
  913. // cards.forEach(id => {
  914. // const valueElement = document.getElementById(`${id}-value`);
  915. // if (valueElement) {
  916. // valueElement.innerHTML = '<div class="loading-spinner-small"></div>';
  917. // }
  918. // });
  919. }
  920. // 显示仪表盘错误
  921. function showDashboardError(message) {
  922. // 在仪表盘上方添加错误通知
  923. const dashboardGrid = document.querySelector('.dashboard-grid');
  924. if (dashboardGrid) {
  925. // 检查是否已存在错误通知,避免重复添加
  926. let errorNotice = document.getElementById('dashboard-error-notice');
  927. if (!errorNotice) {
  928. errorNotice = document.createElement('div');
  929. errorNotice.id = 'dashboard-error-notice';
  930. errorNotice.className = 'dashboard-error-notice';
  931. errorNotice.innerHTML = `
  932. <i class="fas fa-exclamation-circle"></i>
  933. <span>数据加载失败: ${message}</span>
  934. <button onclick="systemStatus.refreshSystemStatus()">重试</button>
  935. `;
  936. dashboardGrid.parentNode.insertBefore(errorNotice, dashboardGrid);
  937. // 5秒后自动隐藏错误通知
  938. setTimeout(() => {
  939. if (errorNotice.parentNode) {
  940. errorNotice.classList.add('fade-out');
  941. setTimeout(() => {
  942. if (errorNotice.parentNode) {
  943. errorNotice.parentNode.removeChild(errorNotice);
  944. }
  945. }, 500);
  946. }
  947. }, 5000);
  948. }
  949. }
  950. }
  951. // --- 明确将需要全局调用的函数暴露到 window.systemStatus ---
  952. // (确保 systemStatus 对象存在)
  953. window.systemStatus = window.systemStatus || {};
  954. // 暴露刷新函数(可能被其他模块或 HTML 调用)
  955. window.systemStatus.refreshSystemStatus = refreshSystemStatus;
  956. // 暴露显示详情函数(被 HTML 调用)
  957. window.systemStatus.showDetailsDialog = showDetailsDialog;
  958. // 暴露显示 Docker 帮助函数(被 HTML 或 dockerManager 调用)
  959. window.systemStatus.showDockerHelp = showDockerHelp;
  960. // 暴露初始化仪表盘函数(可能被 app.js 调用)
  961. window.systemStatus.initDashboard = initDashboard;
  962. // 暴露调试设置函数,方便开发时打开调试
  963. window.systemStatus.setDebug = logger.setDebug;
  964. // --- 新增:用于存储卡片操作回调 ---
  965. window.systemStatus.cardActionCallbacks = {};
  966. // --- 新增:处理卡片操作的通用函数 ---
  967. window.systemStatus.handleCardAction = function(cardId) {
  968. if (window.systemStatus.cardActionCallbacks[cardId]) {
  969. window.systemStatus.cardActionCallbacks[cardId]();
  970. } else {
  971. console.warn(`No action callback registered for card ID: ${cardId}`);
  972. }
  973. };
  974. /* 添加一些基础样式到 CSS (如果 web/style.css 不可用,这里会失败) */
  975. /* 理想情况下,这些样式应该放在 web/style.css */
  976. const customHelpStyles = `
  977. .docker-help-popup .swal2-title {
  978. font-size: 1.5rem;
  979. color: var(--primary-color);
  980. margin-bottom: 1.5rem;
  981. }
  982. /* 强制 SweetAlert 内容区左对齐 */
  983. .docker-help-popup .swal2-html-container {
  984. text-align: left !important;
  985. margin-left: 1rem; /* 可选:增加左边距 */
  986. margin-right: 1rem; /* 可选:增加右边距 */
  987. /* border: 1px solid red !important; /* 临时调试边框 */
  988. }
  989. /* 确保我们自己的内容容器也强制左对齐 */
  990. .docker-help-popup .docker-help-content {
  991. text-align: left !important;
  992. }
  993. .docker-help-swal-content {
  994. /* 这个类可能不再需要,但保留以防万一 */
  995. font-size: 0.95rem;
  996. line-height: 1.6;
  997. }
  998. .docker-help-content ol {
  999. margin-left: 1rem; /* 确保列表相对于左对齐的内容有缩进 */
  1000. }
  1001. /* 确保列表和代码块也继承或设置为左对齐 */
  1002. .docker-help-popup .docker-help-content ol,
  1003. .docker-help-popup .docker-help-content pre {
  1004. text-align: left !important;
  1005. }
  1006. .docker-help-content .code-block {
  1007. background-color: #f8f9fa;
  1008. border: 1px solid #e9ecef;
  1009. padding: 0.5rem 0.8rem;
  1010. border-radius: var(--radius-sm);
  1011. font-family: var(--font-mono);
  1012. font-size: 0.85rem;
  1013. margin-bottom: 0.5rem;
  1014. white-space: pre-wrap; /* 允许换行 */
  1015. word-wrap: break-word; /* 强制换行 */
  1016. }
  1017. .docker-help-content .code-block code {
  1018. background: none;
  1019. padding: 0;
  1020. color: inherit;
  1021. }
  1022. `;
  1023. // 尝试将样式添加到页面 (这是一种不太优雅的方式,最好是在 CSS 文件中定义)
  1024. try {
  1025. const styleSheet = document.createElement("style");
  1026. styleSheet.type = "text/css";
  1027. styleSheet.innerText = customHelpStyles;
  1028. document.head.appendChild(styleSheet);
  1029. } catch (e) {
  1030. console.warn('无法动态添加 Docker 帮助样式:', e);
  1031. }