Răsfoiți Sursa

fix: Fix login error, update container management operations, and update user center operation page.

dqzboy 5 luni în urmă
părinte
comite
08ab90382e

+ 25 - 36
hubcmdui/compatibility-layer.js

@@ -47,15 +47,32 @@ module.exports = function(app) {
     }
   });
   
-  // 停止容器列表接口
-  app.get('/api/stopped-containers', requireLogin, async (req, res) => {
+  // 获取已停止容器接口
+  app.get('/api/stopped-containers', async (req, res) => {
     try {
-      const monitoringService = require('./services/monitoringService');
-      const stoppedContainers = await monitoringService.getStoppedContainers();
-      res.json(stoppedContainers);
-    } catch (error) {
-      logger.error('获取已停止容器列表失败:', error);
-      res.status(500).json({ error: '获取已停止容器列表失败', details: error.message });
+      logger.info('兼容层处理获取已停止容器请求');
+      const { exec } = require('child_process');
+      const util = require('util');
+      const execPromise = util.promisify(exec);
+      
+      const { stdout } = await execPromise('docker ps -f "status=exited" --format "{{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.Status}}"');
+      
+      const containers = stdout.trim().split('\n')
+        .filter(line => line.trim())
+        .map(line => {
+          const [id, name, image, ...statusParts] = line.split('\t');
+          return {
+            id: id.substring(0, 12),
+            name,
+            image,
+            status: statusParts.join(' ')
+          };
+        });
+      
+      res.json(containers);
+    } catch (err) {
+      logger.error('获取已停止容器失败:', err);
+      res.status(500).json({ error: '获取已停止容器失败', details: err.message });
     }
   });
   
@@ -445,34 +462,6 @@ module.exports = function(app) {
     }
   });
   
-  // 获取已停止的容器接口
-  app.get('/api/stopped-containers', requireLogin, async (req, res) => {
-    try {
-      logger.info('兼容层处理获取已停止容器请求');
-      const { exec } = require('child_process');
-      const util = require('util');
-      const execPromise = util.promisify(exec);
-      
-      const { stdout } = await execPromise('docker ps -f "status=exited" --format "{{.ID}}\\t{{.Names}}\\t{{.Status}}"');
-      
-      const containers = stdout.trim().split('\n')
-        .filter(line => line.trim())
-        .map(line => {
-          const [id, name, ...statusParts] = line.split('\t');
-          return {
-            id: id.substring(0, 12),
-            name,
-            status: statusParts.join(' ')
-          };
-        });
-      
-      res.json(containers);
-    } catch (err) {
-      logger.error('获取已停止容器失败:', err);
-      res.status(500).json({ error: '获取已停止容器失败', details: err.message });
-    }
-  });
-  
   // 系统状态接口
   app.get('/api/system-status', requireLogin, async (req, res) => {
     try {

+ 1 - 1
hubcmdui/data/config.json

@@ -25,5 +25,5 @@
     "monitorInterval": 60,
     "isEnabled": false
   },
-  "proxyDomain": "dqzboy.github.io"
+  "proxyDomain": "github.dqzboy.Docker-Proxy"
 }

+ 1 - 1
hubcmdui/documentation/1743542841590.json

@@ -3,5 +3,5 @@
   "content": "# Docker 配置镜像加速\n\n- 修改文件 `/etc/docker/daemon.json`(如果不存在则创建)\n\n```\nsudo mkdir -p /etc/docker\nsudo vi /etc/docker/daemon.json\n{\n  \"registry-mirrors\": [\"https://<代理加速地址>\"]\n}\n\nsudo systemctl daemon-reload\nsudo systemctl restart docker\n```",
   "published": true,
   "createdAt": "2025-04-01T21:27:21.591Z",
-  "updatedAt": "2025-04-01T21:35:20.004Z"
+  "updatedAt": "2025-05-10T06:21:33.539Z"
 }

+ 1 - 1
hubcmdui/documentation/1743543376091.json

@@ -3,5 +3,5 @@
   "content": "# Containerd 配置镜像加速\n\n\n* `/etc/containerd/config.toml`,添加如下的配置:\n\n```bash\n    [plugins.\"io.containerd.grpc.v1.cri\".registry]\n      [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"docker.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"k8s.gcr.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"gcr.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"ghcr.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"quay.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n```",
   "published": true,
   "createdAt": "2025-04-01T21:36:16.092Z",
-  "updatedAt": "2025-04-01T21:36:18.103Z"
+  "updatedAt": "2025-05-10T06:21:38.920Z"
 }

+ 3 - 2
hubcmdui/routes/monitoring.js

@@ -167,15 +167,16 @@ router.get('/stopped-containers', async (req, res) => {
         const util = require('util');
         const execPromise = util.promisify(exec);
         
-        const { stdout } = await execPromise('docker ps -f "status=exited" --format "{{.ID}}\\t{{.Names}}\\t{{.Status}}"');
+        const { stdout } = await execPromise('docker ps -f "status=exited" --format "{{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.Status}}"');
         
         const containers = stdout.trim().split('\n')
             .filter(line => line.trim())
             .map(line => {
-                const [id, name, ...statusParts] = line.split('\t');
+                const [id, name, image, ...statusParts] = line.split('\t');
                 return {
                     id: id.substring(0, 12),
                     name,
+                    image,
                     status: statusParts.join(' ')
                 };
             });

+ 1 - 1
hubcmdui/server.js

@@ -18,7 +18,7 @@ const compatibilityLayer = require('./compatibility-layer');
 const initSystem = require('./scripts/init-system');
 
 // 设置日志级别 (默认INFO, 可通过环境变量设置)
-const logLevel = process.env.LOG_LEVEL || 'INFO';
+const logLevel = process.env.LOG_LEVEL || 'WARN';
 logger.setLogLevel(logLevel);
 logger.info(`日志级别已设置为: ${logLevel}`);
 

+ 55 - 10
hubcmdui/services/dockerService.js

@@ -213,6 +213,33 @@ async function stopContainer(id) {
   }
 }
 
+// 启动容器
+async function startContainer(id) {
+  logger.info(`Attempting to start container ${id}`);
+  const docker = await getDockerConnection();
+  if (!docker) {
+    logger.error(`[startContainer ${id}] Cannot connect to Docker daemon.`);
+    throw new Error('无法连接到 Docker 守护进程');
+  }
+  
+  try {
+    const container = docker.getContainer(id);
+    await container.start();
+    logger.success(`Container ${id} started successfully.`);
+    return { success: true };
+  } catch (error) {
+    logger.error(`[startContainer ${id}] Error starting container:`, error.message || error);
+    // 检查是否是容器不存在的错误
+    if (error.statusCode === 404) {
+      throw new Error(`容器 ${id} 不存在`);
+    } else if (error.statusCode === 304) {
+      logger.warn(`[startContainer ${id}] Container already started.`);
+      return { success: true, message: '容器已启动' }; // 认为已启动也是成功
+    }
+    throw new Error(`启动容器失败: ${error.message}`);
+  }
+}
+
 // 删除容器
 async function deleteContainer(id) {
   const docker = await getDockerConnection();
@@ -387,16 +414,33 @@ async function getStoppedContainers() {
     throw new Error('无法连接到 Docker 守护进程');
   }
   
-  const containers = await docker.listContainers({ 
-    all: true,
-    filters: { status: ['exited', 'dead', 'created'] }
-  });
-  
-  return containers.map(container => ({
-    id: container.Id.slice(0, 12),
-    name: container.Names[0].replace(/^\//, ''),
-    status: container.State
-  }));
+  try {
+    logger.info('正在获取已停止的容器...');
+    const containers = await docker.listContainers({ 
+      all: true,
+      filters: { status: ['exited', 'dead', 'created'] }
+    });
+    
+    logger.info(`找到 ${containers.length} 个已停止的容器`);
+    
+    // 记录每个容器的信息
+    containers.forEach(container => {
+      logger.info(`容器 ID: ${container.Id}, 名称: ${container.Names}, 镜像: ${container.Image}, 状态: ${container.State}`);
+    });
+    
+    const result = containers.map(container => ({
+      id: container.Id.slice(0, 12),
+      name: container.Names[0].replace(/^\//, ''),
+      image: container.Image,
+      status: container.State
+    }));
+    
+    logger.info('已转换容器信息: ' + JSON.stringify(result));
+    return result;
+  } catch (error) {
+    logger.error('获取已停止容器失败:', error);
+    throw error;
+  }
 }
 
 // 获取最近的Docker事件
@@ -468,6 +512,7 @@ module.exports = {
   getContainerStatus,
   restartContainer,
   stopContainer,
+  startContainer,
   deleteContainer,
   updateContainer,
   getContainerLogs,

+ 2 - 2
hubcmdui/users.json

@@ -2,9 +2,9 @@
   "users": [
     {
       "username": "root",
-      "password": "$2b$10$lh1kqJtq3shL2BhMD1LbVOThGeAlPXsDgME/he4ZyDMRupVtj0Hl.",
+      "password": "$2b$10$HBYJPwEB1gdRxcc6Bm1mKukxCC8eyJOZC7sGJN5meghvsBfoQjKtW",
       "loginCount": 0,
-      "lastLogin": "2025-05-08T14:59:22.166Z"
+      "lastLogin": "2025-05-10T11:37:31.774Z"
     }
   ]
 }

+ 678 - 44
hubcmdui/web/admin.html

@@ -320,10 +320,16 @@
             box-shadow: var(--shadow-md);
             padding: 2rem;
             margin-bottom: 2rem;
+            transition: all 0.3s ease;
+        }
+
+        .user-center-card:hover {
+            transform: translateY(-5px);
+            box-shadow: var(--shadow-lg);
         }
 
         .user-center-section {
-            margin-bottom: 2rem;
+            margin-bottom: 1rem;
         }
 
         .user-center-section-title {
@@ -350,7 +356,7 @@
             display: grid;
             grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
             gap: 1.5rem;
-            margin-bottom: 2rem;
+            margin-bottom: 1rem;
         }
 
         .stat-card {
@@ -360,6 +366,19 @@
             padding: 1.5rem;
             border: 1px solid var(--border-light);
             text-align: center;
+            transition: all 0.3s ease;
+        }
+
+        .stat-card:hover {
+            border-color: var(--primary-color);
+            transform: translateY(-3px);
+            box-shadow: var(--shadow-md);
+        }
+
+        .stat-icon {
+            font-size: 1.8rem;
+            color: var(--primary-color);
+            margin-bottom: 0.75rem;
         }
 
         .stat-value {
@@ -374,6 +393,307 @@
             font-size: 0.9rem;
         }
 
+        /* 新增用户个人资料卡片样式 */
+        .user-profile-card {
+            display: flex;
+            align-items: center;
+            background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
+            color: white;
+            border-radius: var(--radius-lg);
+            padding: 2rem;
+            margin-bottom: 2rem;
+            box-shadow: var(--shadow-md);
+            position: relative;
+            overflow: hidden;
+        }
+
+        .user-profile-card::before {
+            content: '';
+            position: absolute;
+            top: -50px;
+            right: -50px;
+            width: 200px;
+            height: 200px;
+            border-radius: 50%;
+            background: rgba(255, 255, 255, 0.1);
+            z-index: 0;
+        }
+
+        .user-profile-card::after {
+            content: '';
+            position: absolute;
+            bottom: -60px;
+            left: 30%;
+            width: 150px;
+            height: 150px;
+            border-radius: 50%;
+            background: rgba(255, 255, 255, 0.1);
+            z-index: 0;
+        }
+
+        .user-profile-avatar {
+            width: 80px;
+            height: 80px;
+            border-radius: 50%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: rgba(255, 255, 255, 0.2);
+            font-size: 2.5rem;
+            margin-right: 1.5rem;
+            z-index: 1;
+        }
+
+        .user-profile-info {
+            flex: 1;
+            z-index: 1;
+        }
+
+        .user-profile-name {
+            font-size: 1.8rem;
+            font-weight: 600;
+            margin: 0 0 0.3rem 0;
+        }
+
+        .user-profile-role {
+            font-size: 1rem;
+            opacity: 0.8;
+            margin: 0 0 1rem 0;
+        }
+
+        .user-profile-badges {
+            display: flex;
+            gap: 0.75rem;
+        }
+
+        .user-badge {
+            padding: 0.3rem 0.75rem;
+            border-radius: 20px;
+            font-size: 0.85rem;
+            display: inline-flex;
+            align-items: center;
+            gap: 0.4rem;
+        }
+
+        .user-badge.admin {
+            background-color: rgba(255, 193, 7, 0.3);
+        }
+
+        .user-badge.active {
+            background-color: rgba(40, 167, 69, 0.3);
+        }
+
+        .user-profile-actions {
+            z-index: 1;
+        }
+
+        .btn-outline {
+            background-color: transparent;
+            border: 1px solid rgba(255, 255, 255, 0.3);
+            color: white;
+            padding: 0.6rem 1rem;
+            border-radius: var(--radius-md);
+            cursor: pointer;
+            transition: all 0.2s ease;
+            display: inline-flex;
+            align-items: center;
+            gap: 0.5rem;
+        }
+
+        .btn-outline:hover {
+            background-color: rgba(255, 255, 255, 0.1);
+            border-color: rgba(255, 255, 255, 0.5);
+        }
+
+        /* 用户信息网格布局 */
+        .user-dashboard-grid {
+            display: grid;
+            grid-template-columns: repeat(auto-fill, minmax(min(100%, 450px), 1fr));
+            gap: 2rem;
+        }
+
+        /* 系统使用情况样式 */
+        .system-usage-stats {
+            display: flex;
+            flex-direction: column;
+            gap: 1.25rem;
+        }
+
+        .usage-stat {
+            width: 100%;
+        }
+
+        .usage-label {
+            display: flex;
+            justify-content: space-between;
+            margin-bottom: 0.6rem;
+            color: var(--text-secondary);
+            font-size: 0.9rem;
+        }
+
+        .usage-label i {
+            color: var(--primary-color);
+            margin-right: 0.5rem;
+        }
+
+        .usage-value {
+            font-weight: 600;
+            color: var(--text-primary);
+        }
+
+        .progress-bar-container {
+            height: 8px;
+            background-color: rgba(0, 0, 0, 0.05);
+            border-radius: 4px;
+            overflow: hidden;
+        }
+
+        .progress-bar {
+            height: 100%;
+            background: linear-gradient(90deg, var(--primary-color) 0%, var(--primary-light) 100%);
+            border-radius: 4px;
+        }
+
+        /* 密码表单样式 */
+        .password-form {
+            display: flex;
+            flex-direction: column;
+            gap: 1.5rem;
+        }
+
+        .form-group {
+            display: flex;
+            flex-direction: column;
+            gap: 0.5rem;
+        }
+
+        .password-input-group {
+            display: flex;
+            position: relative;
+        }
+
+        .password-input {
+            flex: 1;
+            padding: 0.9rem 1rem;
+            border: 1px solid var(--border-color);
+            border-radius: var(--radius-md);
+            color: var(--text-primary);
+            background-color: var(--container-bg);
+            padding-right: 40px;
+        }
+
+        .password-toggle-btn {
+            position: absolute;
+            right: 10px;
+            top: 50%;
+            transform: translateY(-50%);
+            background: none;
+            border: none;
+            color: var(--text-secondary);
+            cursor: pointer;
+            transition: color 0.2s;
+        }
+
+        .password-toggle-btn:hover {
+            color: var(--primary-color);
+        }
+
+        .password-hint {
+            font-size: 0.8rem;
+            color: var(--text-secondary);
+            line-height: 1.3;
+        }
+
+        .password-strength-meter {
+            margin-top: 0.75rem;
+        }
+
+        .strength-bar {
+            height: 6px;
+            background-color: #f0f0f0;
+            border-radius: 3px;
+            margin-bottom: 0.4rem;
+            position: relative;
+        }
+
+        .strength-bar::before {
+            content: '';
+            position: absolute;
+            left: 0;
+            top: 0;
+            height: 100%;
+            border-radius: 3px;
+            width: 0;
+            transition: width 0.3s, background-color 0.3s;
+        }
+
+        .strength-text {
+            font-size: 0.8rem;
+        }
+
+        .password-submit-btn {
+            align-self: flex-start;
+            margin-top: 0.5rem;
+        }
+
+        /* 活动列表样式 */
+        .activity-list {
+            display: flex;
+            flex-direction: column;
+            gap: 1.25rem;
+        }
+
+        .activity-item {
+            display: flex;
+            align-items: center;
+            gap: 1rem;
+            padding-bottom: 1.25rem;
+            border-bottom: 1px solid var(--border-light);
+        }
+
+        .activity-item:last-child {
+            border-bottom: none;
+            padding-bottom: 0;
+        }
+
+        .activity-icon {
+            width: 40px;
+            height: 40px;
+            border-radius: 50%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            color: white;
+            font-size: 1rem;
+        }
+
+        .activity-icon.login {
+            background-color: #4361ee;
+        }
+
+        .activity-icon.container {
+            background-color: #3a86ff;
+        }
+
+        .activity-icon.password {
+            background-color: #f72585;
+        }
+
+        .activity-content {
+            flex: 1;
+        }
+
+        .activity-title {
+            font-weight: 500;
+            color: var(--text-primary);
+            margin-bottom: 0.25rem;
+        }
+
+        .activity-time {
+            font-size: 0.8rem;
+            color: var(--text-secondary);
+        }
+
         .sidebar h2 {
             color: var(--text-primary);
             padding: 0 1.5rem;
@@ -1784,6 +2104,294 @@
         .swal2-html-container div {
             text-align: center !important;
         }
+
+        /* 下拉菜单样式修复 */
+        .btn-group {
+            position: relative;
+            display: inline-flex;
+            vertical-align: middle;
+        }
+        
+        .dropdown-menu {
+            position: absolute;
+            z-index: 1050;
+            display: none;
+            min-width: 180px;
+            width: auto;
+            padding: 0;
+            margin: 0.325rem 0 0;
+            font-size: 0.9rem;
+            color: var(--text-primary);
+            text-align: left;
+            list-style: none;
+            background-color: #ffffff;
+            background-clip: padding-box;
+            border: 1px solid rgba(0, 0, 0, 0.08);
+            border-radius: 8px;
+            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+            opacity: 0;
+            visibility: hidden;
+            transform: translateY(10px) scale(0.98);
+            transition: opacity 0.25s ease, transform 0.25s ease, visibility 0.25s ease;
+        }
+        
+        .dropdown-menu.show {
+            display: block;
+            opacity: 1;
+            visibility: visible;
+            transform: translateY(0) scale(1);
+        }
+        
+        /* 当下拉菜单在按钮右侧显示时 */
+        .dropdown-menu.dropdown-menu-right {
+            transform: translateX(10px) scale(0.98);
+        }
+        
+        /* 当下拉菜单在按钮上方显示时 */
+        .dropdown-menu.dropdown-menu-top {
+            transform: translateY(-10px) scale(0.98);
+        }
+        
+        /* 当下拉菜单在按钮右侧且显示时 */
+        .dropdown-menu.dropdown-menu-right.show {
+            transform: translateX(0) scale(1);
+        }
+        
+        /* 当下拉菜单在按钮上方且显示时 */
+        .dropdown-menu.dropdown-menu-top.show {
+            transform: translateY(0) scale(1);
+        }
+        
+        /* 右对齐的下拉菜单 */
+        .dropdown-menu-end {
+            right: 0;
+            left: auto;
+        }
+        
+        .dropdown-header {
+            display: block;
+            padding: 0.5rem 1rem;
+            margin-bottom: 0;
+            font-size: 0.8rem;
+            color: #6c757d;
+            white-space: nowrap;
+            background-color: #f8f9fa;
+            font-weight: 500;
+        }
+        
+        .dropdown-menu .dropdown-item {
+            display: flex;
+            align-items: center;
+            width: 100%;
+            padding: 0.7rem 1.25rem;
+            clear: both;
+            font-weight: 400;
+            color: #495057;
+            text-align: inherit;
+            text-decoration: none;
+            white-space: nowrap;
+            background-color: transparent;
+            border: 0;
+            transition: all 0.2s ease;
+        }
+        
+        .dropdown-menu .dropdown-item i {
+            margin-right: 10px;
+            width: 18px;
+            text-align: center;
+            font-size: 0.95rem;
+            opacity: 0.8;
+        }
+        
+        .dropdown-menu .dropdown-item:hover, 
+        .dropdown-menu .dropdown-item:focus {
+            color: #1e70eb;
+            background-color: #f1f7ff;
+            text-decoration: none;
+        }
+        
+        .dropdown-menu .dropdown-item.active,
+        .dropdown-menu .dropdown-item:active {
+            background-color: #e8f1ff;
+            color: #1e70eb;
+        }
+        
+        .dropdown-menu .dropdown-item:hover i, 
+        .dropdown-menu .dropdown-item:focus i {
+            opacity: 1;
+        }
+        
+        .dropdown-divider {
+            height: 0;
+            margin: 0;
+            overflow: hidden;
+            border-top: 1px solid rgba(0, 0, 0, 0.05);
+        }
+        
+        /* 美化操作按钮 */
+        .action-cell .btn-group .btn-primary {
+            background: linear-gradient(to bottom, #4a7bff, #3d66e3);
+            border: none;
+            box-shadow: 0 2px 5px rgba(61, 124, 244, 0.2);
+            padding: 0.45rem 1rem;
+            font-weight: 500;
+            transition: all 0.2s ease;
+            border-radius: 6px;
+        }
+        
+        .action-cell .btn-group .btn-primary:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 4px 8px rgba(61, 124, 244, 0.3);
+            background: linear-gradient(to bottom, #5a88ff, #4a7bff);
+        }
+        
+        .action-cell .btn-group .btn-primary:active {
+            transform: translateY(0);
+        }
+        
+        /* 右对齐的下拉菜单 */
+        .dropdown-menu-end {
+            right: 0;
+            left: auto;
+        }
+        
+        /* 美化表格中的操作列,确保有足够空间显示弹出菜单 */
+        .action-cell {
+            min-width: 120px;
+            position: relative;
+        }
+        
+        /* 确保菜单项样式美观 */
+        .dropdown-item {
+            display: flex;
+            align-items: center;
+            width: 100%;
+            padding: 0.7rem 1.25rem;
+            clear: both;
+            font-weight: 400;
+            color: #495057;
+            text-align: inherit;
+            text-decoration: none;
+            white-space: nowrap;
+            background-color: transparent;
+            border: 0;
+            transition: all 0.2s ease;
+        }
+        
+        .dropdown-item i {
+            margin-right: 10px;
+            width: 18px;
+            text-align: center;
+            font-size: 0.95rem;
+            opacity: 0.8;
+        }
+        
+        .dropdown-item:hover, .dropdown-item:focus {
+            color: #1e70eb;
+            background-color: rgba(30, 112, 235, 0.08);
+            text-decoration: none;
+        }
+        
+        .dropdown-item:hover i, .dropdown-item:focus i {
+            opacity: 1;
+        }
+        
+        .dropdown-divider {
+            height: 0;
+            margin: 0.5rem 0;
+            overflow: hidden;
+            border-top: 1px solid rgba(0, 0, 0, 0.05);
+        }
+        
+        /* 菜单项图标美化 */
+        .dropdown-item .fa-file-alt {
+            color: #17a2b8;
+        }
+        
+        .dropdown-item .fa-info-circle {
+            color: #6c757d;
+        }
+        
+        .dropdown-item .fa-stop {
+            color: #dc3545;
+        }
+        
+        .dropdown-item .fa-play {
+            color: #28a745;
+        }
+        
+        .dropdown-item .fa-sync-alt {
+            color: #ffc107;
+        }
+        
+        .dropdown-item .fa-trash-alt {
+            color: #dc3545;
+        }
+        
+        .dropdown-item .fa-cloud-download-alt {
+            color: #17a2b8;
+        }
+
+        /* 原生select下拉框样式美化 */
+        .simple-dropdown {
+            appearance: none;
+            -webkit-appearance: none;
+            -moz-appearance: none;
+            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23495057' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+            background-repeat: no-repeat;
+            background-position: right 0.75rem center;
+            background-size: 12px;
+            width: 100%;
+            padding: 0.45rem 2rem 0.45rem 0.75rem;
+            font-size: 0.875rem;
+            font-weight: 400;
+            color: #495057;
+            border: 1px solid #ced4da;
+            border-radius: 6px;
+            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+            transition: all 0.2s ease-in-out;
+            cursor: pointer;
+        }
+
+        .simple-dropdown:focus {
+            border-color: #80bdff;
+            outline: 0;
+            box-shadow: 0 0 0 0.2rem rgba(0,123,255,0.25);
+        }
+
+        .simple-dropdown:hover {
+            border-color: #adb5bd;
+        }
+
+        .simple-dropdown optgroup {
+            font-weight: 600;
+            color: #343a40;
+            background-color: #f8f9fa;
+            padding: 5px;
+        }
+
+        .simple-dropdown option {
+            font-weight: normal;
+            padding: 8px;
+            background-color: #fff;
+            color: #495057;
+        }
+
+        .simple-dropdown option:hover,
+        .simple-dropdown option:focus {
+            background-color: #f1f7ff;
+            color: #1e70eb;
+        }
+
+        /* Action按钮样式保留,以便保持兼容 */
+        .action-cell .btn-group {
+            display: inline-block;
+            width: 100%;
+        }
+
+        .action-cell .btn-group .btn-primary {
+            display: none; /* 隐藏原始按钮,由select替代 */
+        }
     </style>
 </head>
 <body>
@@ -1856,13 +2464,6 @@
                     <div class="dashboard-grid">
                         <!-- 仪表板卡片将由 systemStatus.initDashboard() 动态生成 -->
                     </div>
-
-                    <div class="dashboard-card">
-                        <h3 class="card-title">最近容器操作</h3>
-                        <table id="recentActivitiesTable">
-                            <!-- 活动表内容将由 systemStatus.refreshSystemStatus() 动态更新 -->
-                        </table>
-                    </div>
                 </div>
 
                 <!-- 基本配置 -->
@@ -1873,13 +2474,13 @@
                             <label for="logoUrl">Logo URL: (可选)</label>
                             <input type="url" id="logoUrl" name="logoUrl" class="form-control">
                         </div>
-                        <button type="button" class="btn btn-primary" onclick="saveConfig({logo: document.getElementById('logoUrl').value})">保存 Logo</button>
+                        <button type="button" class="btn btn-primary" onclick="validateAndSaveConfig('logo')">保存 Logo</button>
                     
                         <div class="form-group" style="margin-top: 2rem;">
                             <label for="proxyDomain">Docker镜像代理地址: (必填)</label>
                             <input type="text" id="proxyDomain" name="proxyDomain" class="form-control" required>
                         </div>
-                        <button type="button" class="btn btn-primary" onclick="saveConfig({proxyDomain: document.getElementById('proxyDomain').value})">保存代理地址</button>
+                        <button type="button" class="btn btn-primary" onclick="validateAndSaveConfig('proxy')">保存代理地址</button>
                     </div>
                 </div>
                 
@@ -2046,9 +2647,10 @@
                         <table id="stoppedContainersTable" class="container-table">
                             <thead>
                                 <tr>
-                                    <th>容器 ID</th>
-                                    <th>名称</th>
-                                    <th>状态</th>
+                                    <th>容器ID</th>
+                                    <th>容器名称</th>
+                                    <th>镜像名称</th>
+                                    <th>运行状态</th>
                                 </tr>
                             </thead>
                             <tbody id="stoppedContainersBody"></tbody>
@@ -2066,40 +2668,67 @@
                         <button class="btn btn-primary" id="ucLogoutBtn" style="display: inline-block;">退出登录</button>
                     </div>
                     
-                    <div class="user-center-card">
-                        <div class="user-center-section">
-                            <h2 class="user-center-section-title">账户信息</h2>
-                            <div class="user-stats">
-                                <div class="stat-card">
-                                    <div class="stat-value" id="loginCount">--</div>
-                                    <div class="stat-label">登录次数</div>
-                                </div>
-                                <div class="stat-card">
-                                    <div class="stat-value" id="lastLogin">--</div>
-                                    <div class="stat-label">上次登录</div>
-                                </div>
-                                <div class="stat-card">
-                                    <div class="stat-value" id="accountAge">--</div>
-                                    <div class="stat-label">账户天数</div>
+                    <!-- 个人资料卡片 -->
+                    <div class="user-profile-card">
+                        <div class="user-profile-avatar">
+                            <i class="fas fa-user-circle"></i>
+                        </div>
+                        <div class="user-profile-info">
+                            <h2 class="user-profile-name" id="profileUsername">管理员</h2>
+                            <p class="user-profile-role">系统管理员</p>
+                            <div class="user-profile-badges">
+                                <span class="user-badge admin"><i class="fas fa-shield-alt"></i> 管理员</span>
+                                <span class="user-badge active"><i class="fas fa-check-circle"></i> 活跃</span>
+                            </div>
+                        </div>
+                        <div class="user-profile-actions">
+                            <button class="btn btn-outline" onclick="userCenter.refreshUserInfo()"><i class="fas fa-sync-alt"></i> 刷新</button>
+                        </div>
+                    </div>
+                    
+                    <div class="user-dashboard-grid">
+                        <!-- 账户信息卡片 -->
+                        <div class="user-center-card">
+                            <div class="user-center-section">
+                                <h2 class="user-center-section-title">账户信息</h2>
+                                <div class="user-stats">
+                                    <div class="stat-card">
+                                        <div class="stat-icon"><i class="fas fa-sign-in-alt"></i></div>
+                                        <div class="stat-value" id="loginCount">--</div>
+                                        <div class="stat-label">登录次数</div>
+                                    </div>
+                                    <div class="stat-card">
+                                        <div class="stat-icon"><i class="fas fa-clock"></i></div>
+                                        <div class="stat-value" id="lastLogin">--</div>
+                                        <div class="stat-label">上次登录</div>
+                                    </div>
+                                    <div class="stat-card">
+                                        <div class="stat-icon"><i class="fas fa-calendar-alt"></i></div>
+                                        <div class="stat-value" id="accountAge">--</div>
+                                        <div class="stat-label">账户天数</div>
+                                    </div>
                                 </div>
                             </div>
-                            
-                            <!-- 移除用户详细信息部分 -->
                         </div>
                         
-                        <div class="user-center-section">
-                            <h2 class="user-center-section-title">修改密码</h2>
-                            <form id="changePasswordForm">
-                                <label for="ucCurrentPassword">当前密码</label>
-                                <input type="password" id="ucCurrentPassword" name="currentPassword">
-                                <label for="ucNewPassword">新密码</label>
-                                <span class="password-hint" id="ucPasswordHint">密码必须包含至少一个字母、一个数字和一个特殊字符,长度在8到16个字符之间</span>
-                                <input type="password" id="ucNewPassword" name="newPassword" oninput="userCenter.checkUcPasswordStrength()">
-                                <label for="ucConfirmPassword">确认新密码</label>
-                                <input type="password" id="ucConfirmPassword" name="confirmPassword">
-                                <span id="ucPasswordStrength" style="color: red;"></span>
-                                <button type="submit" class="btn btn-primary">修改密码</button>
-                            </form>
+                        <!-- 密码修改卡片 -->
+                        <div class="user-center-card">
+                            <div class="user-center-section">
+                                <h2 class="user-center-section-title">修改密码</h2>
+                                <form id="changePasswordForm">
+                                    <label for="ucCurrentPassword">当前密码</label>
+                                    <input type="password" id="ucCurrentPassword" name="currentPassword">
+                                    <label for="ucNewPassword">新密码</label>
+                                    <span class="password-hint" id="ucPasswordHint">密码必须包含至少一个字母、一个数字和一个特殊字符,长度在8到16个字符之间</span>
+                                    <input type="password" id="ucNewPassword" name="newPassword" oninput="userCenter.checkUcPasswordStrength()">
+                                    <label for="ucConfirmPassword">确认新密码</label>
+                                    <input type="password" id="ucConfirmPassword" name="confirmPassword">
+                                    <div style="display: flex; align-items: center; margin-top: 10px;">
+                                        <button type="submit" class="btn btn-primary">修改密码</button>
+                                        <span id="ucPasswordStrength" style="color: red; white-space: nowrap; display: inline-block; margin-left: 15px;"></span>
+                                    </div>
+                                </form>
+                            </div>
                         </div>
                     </div>
                 </div>
@@ -2190,6 +2819,11 @@
             window.saveConfig = window.app ? window.app.saveConfig : function(data) {
                 console.error('saveConfig未定义');
             };
+            
+            // 确保validateAndSaveConfig可用
+            window.validateAndSaveConfig = window.app ? window.app.validateAndSaveConfig : function(type) {
+                console.error('validateAndSaveConfig未定义');
+            };
         });
     </script>
 </body>

+ 77 - 8
hubcmdui/web/js/app.js

@@ -94,6 +94,9 @@ async function initializeModules() {
         // 加载监控配置
         await loadMonitoringConfig();
         
+        // 加载已停止的容器列表
+        refreshStoppedContainers();
+        
         // 显示默认页面 - 使用core中的showSection函数
         core.showSection('dashboard');
         
@@ -141,6 +144,10 @@ function loadMonitoringConfig() {
             document.getElementById('toggleMonitoringBtn').textContent = 
                 config.isEnabled ? '禁用监控' : '启用监控';
             
+            // 添加通知类型选择变化的监听器
+            const notificationTypeSelect = document.getElementById('notificationType');
+            notificationTypeSelect.addEventListener('change', toggleNotificationFields);
+            
             // console.log('监控配置加载完成');
         })
         .catch(error => {
@@ -391,22 +398,55 @@ function refreshStoppedContainers() {
     fetch('/api/stopped-containers')
         .then(response => {
             if (!response.ok) throw new Error('获取已停止容器列表失败');
-            return response.json();
+            // 保存原始响应文本用于调试
+            return response.text().then(text => {
+                try {
+                    // 尝试解析为JSON
+                    const data = JSON.parse(text);
+                    
+                    // 打印原始响应
+                    console.log('原始响应:', text);
+                    console.log('解析后对象:', data);
+                    
+                    // 打印镜像字段
+                    if (Array.isArray(data)) {
+                        data.forEach(container => {
+                            console.log('容器镜像字段:', container.image, 
+                                       '类型:', typeof container.image,
+                                       'JSON字符串:', JSON.stringify(container));
+                        });
+                    }
+                    
+                    return data;
+                } catch (e) {
+                    console.error('解析JSON失败:', e, '原始文本:', text);
+                    throw new Error('解析响应失败');
+                }
+            });
         })
         .then(containers => {
+            // 添加调试信息
+            console.log('已停止的容器数据:', JSON.stringify(containers, null, 2));
+            
             const tbody = document.getElementById('stoppedContainersBody');
             tbody.innerHTML = '';
             
             if (containers.length === 0) {
-                tbody.innerHTML = '<tr><td colspan="3" style="text-align: center;">没有已停止的容器</td></tr>';
+                tbody.innerHTML = '<tr><td colspan="4" style="text-align: center;">没有已停止的容器</td></tr>';
                 return;
             }
             
             containers.forEach(container => {
+                // 调试单个容器数据
+                console.log('容器数据:', container.id, container.name, 
+                           '镜像:', container.image, 
+                           '状态:', container.status);
+                
                 const row = `
                     <tr>
                         <td>${container.id}</td>
                         <td>${container.name}</td>
+                        <td>${container.image ? container.image : '未知'}</td>
                         <td>${container.status}</td>
                     </tr>
                 `;
@@ -416,7 +456,7 @@ function refreshStoppedContainers() {
         .catch(error => {
             console.error('获取已停止容器列表失败:', error);
             document.getElementById('stoppedContainersBody').innerHTML = 
-                '<tr><td colspan="3" style="text-align: center; color: red;">获取已停止容器列表失败</td></tr>';
+                '<tr><td colspan="4" style="text-align: center; color: red;">获取已停止容器列表失败</td></tr>';
         });
 }
 
@@ -457,6 +497,35 @@ function saveConfig(configData) {
     });
 }
 
+// 验证输入并保存配置
+function validateAndSaveConfig(type) {
+    if (type === 'logo') {
+        const logoUrl = document.getElementById('logoUrl').value.trim();
+        if (!logoUrl) {
+            Swal.fire({
+                icon: 'error',
+                title: '输入错误',
+                text: 'Logo URL不能为空!',
+                confirmButtonText: '确定'
+            });
+            return;
+        }
+        saveConfig({logo: logoUrl});
+    } else if (type === 'proxy') {
+        const proxyDomain = document.getElementById('proxyDomain').value.trim();
+        if (!proxyDomain) {
+            Swal.fire({
+                icon: 'error',
+                title: '输入错误',
+                text: 'Docker镜像代理地址不能为空,这是必填项!',
+                confirmButtonText: '确定'
+            });
+            return;
+        }
+        saveConfig({proxyDomain: proxyDomain});
+    }
+}
+
 // 加载基本配置
 function loadBasicConfig() {
     fetch('/api/config')
@@ -482,14 +551,14 @@ function loadBasicConfig() {
         });
 }
 
-// 暴露给全局作用域的函数
+// 导出需要的函数到window.app对象
 window.app = {
-    loadMonitoringConfig,
-    loadBasicConfig,
-    toggleNotificationFields,
     saveMonitoringConfig,
     testNotification,
     toggleMonitoring,
+    toggleNotificationFields,
     refreshStoppedContainers,
-    saveConfig
+    saveConfig,
+    loadBasicConfig,
+    validateAndSaveConfig
 };

+ 2 - 1
hubcmdui/web/js/core.js

@@ -516,5 +516,6 @@ window.core = {
     formatDateTime,
     debounce,
     throttle,
-    toggleLoadingState
+    toggleLoadingState,
+    initEventListeners
 };

+ 494 - 104
hubcmdui/web/js/dockerManager.js

@@ -29,37 +29,169 @@ const dockerManager = {
 
     // 初始化Bootstrap下拉菜单组件
     initDropdowns: function() {
-        // 减少日志输出
-        // console.log('[dockerManager] Initializing Bootstrap dropdowns...');
-        
-        // 直接初始化,不使用setTimeout避免延迟导致的问题
         try {
-            // 动态初始化所有下拉菜单
+            console.log('[dockerManager] 初始化下拉菜单...');
+            
+            // 动态初始化所有下拉菜单按钮
             const dropdownElements = document.querySelectorAll('[data-bs-toggle="dropdown"]');
+            console.log(`[dockerManager] 找到 ${dropdownElements.length} 个下拉元素`);
+            
             if (dropdownElements.length === 0) {
                 return; // 如果没有找到下拉元素,直接返回
             }
             
-            if (window.bootstrap && window.bootstrap.Dropdown) {
+            // 尝试使用所有可能的Bootstrap初始化方法
+            if (window.bootstrap && typeof window.bootstrap.Dropdown !== 'undefined') {
+                console.log('[dockerManager] 使用 Bootstrap 5 初始化下拉菜单');
                 dropdownElements.forEach(el => {
                     try {
                         new window.bootstrap.Dropdown(el);
                     } catch (e) {
-                        // 静默处理错误,不要输出到控制台
+                        console.error('Bootstrap 5 下拉菜单初始化错误:', e);
                     }
                 });
+            } else if (typeof $ !== 'undefined' && typeof $.fn.dropdown !== 'undefined') {
+                console.log('[dockerManager] 使用 jQuery Bootstrap 初始化下拉菜单');
+                $(dropdownElements).dropdown();
             } else {
-                console.warn('Bootstrap Dropdown 组件未找到,将尝试使用jQuery初始化');
-                // 尝试使用jQuery初始化(如果存在)
-                if (window.jQuery) {
-                    window.jQuery('[data-bs-toggle="dropdown"]').dropdown();
-                }
+                console.warn('[dockerManager] 未找到Bootstrap下拉菜单组件,将使用手动下拉实现');
+                this.setupManualDropdowns();
             }
         } catch (error) {
-            // 静默处理错误
+            console.error('[dockerManager] 初始化下拉菜单错误:', error);
+            // 失败时使用备用方案
+            this.setupManualDropdowns();
         }
     },
 
+    // 手动实现下拉菜单功能(备用方案)
+    setupManualDropdowns: function() {
+        console.log('[dockerManager] 设置手动下拉菜单...');
+        
+        // 为所有下拉菜单按钮添加点击事件
+        document.querySelectorAll('.btn-group .dropdown-toggle').forEach(button => {
+            // 移除旧事件监听器
+            const newButton = button.cloneNode(true);
+            button.parentNode.replaceChild(newButton, button);
+            
+            // 添加新事件监听器
+            newButton.addEventListener('click', function(e) {
+                e.preventDefault();
+                e.stopPropagation();
+                
+                // 查找关联的下拉菜单
+                const dropdownMenu = this.nextElementSibling;
+                if (!dropdownMenu || !dropdownMenu.classList.contains('dropdown-menu')) {
+                    return;
+                }
+                
+                // 切换显示/隐藏
+                const isVisible = dropdownMenu.classList.contains('show');
+                
+                // 先隐藏所有其他打开的下拉菜单
+                document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
+                    menu.classList.remove('show');
+                });
+                
+                // 切换当前菜单
+                if (!isVisible) {
+                    dropdownMenu.classList.add('show');
+                    
+                    // 计算位置 - 精确计算确保菜单位置更美观
+                    const buttonRect = newButton.getBoundingClientRect();
+                    const tableCell = newButton.closest('td');
+                    const tableCellRect = tableCell ? tableCell.getBoundingClientRect() : buttonRect;
+                    
+                    // 设置最小宽度,确保下拉菜单够宽
+                    const minWidth = Math.max(180, buttonRect.width * 1.5);
+                    dropdownMenu.style.minWidth = `${minWidth}px`;
+                    
+                    // 设置绝对定位
+                    dropdownMenu.style.position = 'absolute';
+                    
+                    // 根据屏幕空间计算最佳位置
+                    const viewportWidth = window.innerWidth;
+                    const viewportHeight = window.innerHeight;
+                    const spaceRight = viewportWidth - buttonRect.right;
+                    const spaceBottom = viewportHeight - buttonRect.bottom;
+                    const spaceAbove = buttonRect.top;
+                    
+                    // 先移除所有位置相关的类
+                    dropdownMenu.classList.remove('dropdown-menu-top', 'dropdown-menu-right');
+                    
+                    // 设置为右对齐,且显示在按钮上方
+                    dropdownMenu.style.right = '0';
+                    dropdownMenu.style.left = 'auto';
+                    
+                    // 计算菜单高度 (假设每个菜单项高度为40px,分隔线10px)
+                    const menuItemCount = dropdownMenu.querySelectorAll('.dropdown-item').length;
+                    const dividerCount = dropdownMenu.querySelectorAll('.dropdown-divider').length;
+                    const estimatedMenuHeight = (menuItemCount * 40) + (dividerCount * 10) + 20; // 加上padding
+                    
+                    // 优先显示在按钮上方,如果空间不足则显示在下方
+                    if (spaceAbove >= estimatedMenuHeight && spaceAbove > spaceBottom) {
+                        // 显示在按钮上方
+                        dropdownMenu.style.bottom = `${buttonRect.height + 5}px`; // 5px间距
+                        dropdownMenu.style.top = 'auto';
+                        // 设置动画原点为底部
+                        dropdownMenu.style.transformOrigin = 'bottom right';
+                        // 添加上方显示的类
+                        dropdownMenu.classList.add('dropdown-menu-top');
+                    } else {
+                        // 显示在右侧而不是正下方
+                        if (spaceRight >= minWidth && tableCellRect.width > buttonRect.width + 20) {
+                            // 有足够的右侧空间,显示在按钮右侧
+                            dropdownMenu.style.top = '0';
+                            dropdownMenu.style.left = `${buttonRect.width + 5}px`; // 5px间距
+                            dropdownMenu.style.right = 'auto';
+                            dropdownMenu.style.bottom = 'auto';
+                            dropdownMenu.style.transformOrigin = 'left top';
+                            // 添加右侧显示的类
+                            dropdownMenu.classList.add('dropdown-menu-right');
+                        } else {
+                            // 显示在按钮下方,但尝试右对齐
+                            dropdownMenu.style.top = `${buttonRect.height + 5}px`; // 5px间距
+                            dropdownMenu.style.bottom = 'auto';
+                            
+                            // 如果下拉菜单宽度超过右侧可用空间,则左对齐显示
+                            if (minWidth > spaceRight) {
+                                dropdownMenu.style.right = 'auto';
+                                dropdownMenu.style.left = '0';
+                            } else {
+                                // 继续使用右对齐
+                                dropdownMenu.classList.add('dropdown-menu-end');
+                            }
+                            
+                            dropdownMenu.style.transformOrigin = 'top right';
+                        }
+                    }
+                    
+                    // 清除其他可能影响布局的样式
+                    dropdownMenu.style.margin = '0';
+                    dropdownMenu.style.maxHeight = '85vh';
+                    dropdownMenu.style.overflowY = 'auto';
+                    dropdownMenu.style.zIndex = '1050'; // 确保在表格上方
+                }
+                
+                // 点击其他区域关闭下拉菜单
+                const closeHandler = function(event) {
+                    if (!dropdownMenu.contains(event.target) && !newButton.contains(event.target)) {
+                        dropdownMenu.classList.remove('show');
+                        document.removeEventListener('click', closeHandler);
+                    }
+                };
+                
+                // 只在打开菜单时添加全局点击监听
+                if (!isVisible) {
+                    // 延迟一点添加事件,避免立即触发
+                    setTimeout(() => {
+                        document.addEventListener('click', closeHandler);
+                    }, 10);
+                }
+            });
+        });
+    },
+
     // 显示表格加载状态 - 保持,用于初始渲染和刷新
     showLoadingState() {
         const table = document.getElementById('dockerStatusTable');
@@ -89,6 +221,9 @@ const dockerManager = {
                     const refreshBtn = document.getElementById('refreshDockerBtn');
                     if (refreshBtn) {
                         refreshBtn.addEventListener('click', () => {
+                            // 显示加载状态,提高用户体验
+                            this.showRefreshingState(refreshBtn);
+                            
                             if (window.systemStatus && typeof window.systemStatus.refreshSystemStatus === 'function') {
                                 window.systemStatus.refreshSystemStatus();
                             }
@@ -107,11 +242,11 @@ const dockerManager = {
             if (thead) {
                 thead.innerHTML = ` 
                     <tr>
-                        <th style="width: 120px;">容器ID</th>
-                        <th style="width: 25%;">容器名称</th>
-                        <th style="width: 35%;">镜像名称</th>
-                        <th style="width: 100px;">运行状态</th>
-                        <th style="width: 150px;">操作</th>
+                        <th style="width: 12%;">容器ID</th>
+                        <th style="width: 18%;">容器名称</th>
+                        <th style="width: 30%;">镜像名称</th>
+                        <th style="width: 15%;">运行状态</th>
+                        <th style="width: 15%;">操作</th>
                     </tr>
                 `;
             }
@@ -127,7 +262,130 @@ const dockerManager = {
                     </td>
                 </tr>
             `;
+            
+            // 添加表格样式
+            this.applyTableStyles(table);
+        }
+    },
+
+    // 新增:显示刷新中状态
+    showRefreshingState(refreshBtn) {
+        if (!refreshBtn) return;
+        
+        // 保存原始按钮内容
+        const originalContent = refreshBtn.innerHTML;
+        
+        // 更改为加载状态
+        refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> 刷新中...';
+        refreshBtn.disabled = true;
+        refreshBtn.classList.add('refreshing');
+        
+        // 添加样式使按钮看起来正在加载
+        const style = document.createElement('style');
+        style.textContent = `
+            .btn.refreshing {
+                opacity: 0.8;
+                cursor: not-allowed;
+            }
+            @keyframes pulse {
+                0% { opacity: 0.6; }
+                50% { opacity: 1; }
+                100% { opacity: 0.6; }
+            }
+            .btn.refreshing i {
+                animation: pulse 1.5s infinite;
+            }
+            .table-overlay {
+                position: absolute;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                background-color: rgba(255, 255, 255, 0.7);
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                align-items: center;
+                z-index: 10;
+                border-radius: 0.25rem;
+            }
+            .table-overlay .spinner {
+                width: 40px;
+                height: 40px;
+                border: 4px solid #f3f3f3;
+                border-top: 4px solid #3498db;
+                border-radius: 50%;
+                animation: spin 1s linear infinite;
+                margin-bottom: 10px;
+            }
+            @keyframes spin {
+                0% { transform: rotate(0deg); }
+                100% { transform: rotate(360deg); }
+            }
+        `;
+        
+        // 检查是否已经添加了样式
+        const existingStyle = document.querySelector('style[data-for="refresh-button"]');
+        if (!existingStyle) {
+            style.setAttribute('data-for', 'refresh-button');
+            document.head.appendChild(style);
+        }
+        
+        // 获取表格和容器
+        const table = document.getElementById('dockerStatusTable');
+        const tableContainer = document.getElementById('dockerTableContainer');
+        
+        // 移除任何现有的覆盖层
+        const existingOverlay = document.querySelector('.table-overlay');
+        if (existingOverlay) {
+            existingOverlay.remove();
+        }
+        
+        // 创建一个覆盖层而不是替换表格内容
+        if (table) {
+            // 设置表格容器为相对定位,以便正确放置覆盖层
+            if (tableContainer) {
+                tableContainer.style.position = 'relative';
+            } else {
+                table.parentNode.style.position = 'relative';
+            }
+            
+            // 创建覆盖层
+            const overlay = document.createElement('div');
+            overlay.className = 'table-overlay';
+            overlay.innerHTML = `
+                <div class="spinner"></div>
+                <p>正在更新容器列表...</p>
+            `;
+            
+            // 获取表格的位置并设置覆盖层
+            const tableRect = table.getBoundingClientRect();
+            overlay.style.width = `${table.offsetWidth}px`;
+            overlay.style.height = `${table.offsetHeight}px`;
+            
+            // 将覆盖层添加到表格容器
+            if (tableContainer) {
+                tableContainer.appendChild(overlay);
+            } else {
+                table.parentNode.appendChild(overlay);
+            }
         }
+        
+        // 设置超时,防止永久加载状态
+        setTimeout(() => {
+            // 如果按钮仍处于加载状态,恢复为原始状态
+            if (refreshBtn.classList.contains('refreshing')) {
+                refreshBtn.innerHTML = originalContent;
+                refreshBtn.disabled = false;
+                refreshBtn.classList.remove('refreshing');
+                
+                // 移除覆盖层
+                const overlay = document.querySelector('.table-overlay');
+                if (overlay) {
+                    overlay.remove();
+                }
+            }
+        }, 10000); // 10秒超时
     },
 
     // 渲染容器表格 - 核心渲染函数,由 systemStatus 调用
@@ -135,6 +393,20 @@ const dockerManager = {
         // 减少详细日志输出
         // console.log(`[dockerManager] Rendering containers table. Containers count: ${containers ? containers.length : 0}`);
         
+        // 重置刷新按钮状态
+        const refreshBtn = document.getElementById('refreshDockerBtn');
+        if (refreshBtn && refreshBtn.classList.contains('refreshing')) {
+            refreshBtn.innerHTML = '<i class="fas fa-sync-alt me-1"></i> 刷新列表';
+            refreshBtn.disabled = false;
+            refreshBtn.classList.remove('refreshing');
+            
+            // 移除覆盖层
+            const overlay = document.querySelector('.table-overlay');
+            if (overlay) {
+                overlay.remove();
+            }
+        }
+        
         const tbody = document.getElementById('dockerStatusTableBody');
         if (!tbody) {
             return;
@@ -149,11 +421,11 @@ const dockerManager = {
                 const newThead = thead || document.createElement('thead');
                 newThead.innerHTML = ` 
                     <tr>
-                        <th style="width: 120px;">容器ID</th>
-                        <th style="width: 25%;">容器名称</th>
-                        <th style="width: 35%;">镜像名称</th>
-                        <th style="width: 100px;">运行状态</th>
-                        <th style="width: 150px;">操作</th>
+                        <th style="width: 12%;">容器ID</th>
+                        <th style="width: 18%;">容器名称</th>
+                        <th style="width: 30%;">镜像名称</th>
+                        <th style="width: 15%;">运行状态</th>
+                        <th style="width: 15%;">操作</th>
                     </tr>
                 `;
                 
@@ -161,6 +433,9 @@ const dockerManager = {
                     table.insertBefore(newThead, tbody);
                 }
             }
+            
+            // 应用表格样式
+            this.applyTableStyles(table);
         }
 
         // 1. 检查 Docker 服务状态
@@ -198,61 +473,50 @@ const dockerManager = {
             
             // 添加lowerStatus变量定义,修复错误
             const lowerStatus = status.toLowerCase();
-
-            // 替换下拉菜单实现为直接的操作按钮
-            let actionButtons = '';
             
-            // 基本操作:查看日志和详情
-            actionButtons += `
-                <button class="btn btn-sm btn-outline-info mb-1 mr-1 action-logs" data-id="${containerId}" data-name="${containerName}">
-                    <i class="fas fa-file-alt"></i> 日志
-                </button>
-                <button class="btn btn-sm btn-outline-secondary mb-1 mr-1 action-details" data-id="${containerId}">
-                    <i class="fas fa-info-circle"></i> 详情
-                </button>
+            // 创建按钮组,使用标准Bootstrap 5下拉菜单语法
+            let actionButtons = `
+                <div class="btn-group">
+                    <button type="button" class="btn btn-sm btn-primary dropdown-toggle simple-dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
+                        操作
+                    </button>
+                    <select class="simple-dropdown">
+                        <option value="" selected disabled>选择操作</option>
+                        <option class="dropdown-item action-logs" data-id="${containerId}" data-name="${containerName}">查看日志</option>
             `;
             
-            // 根据状态显示不同操作
+            // 根据状态添加不同的操作选项
             if (lowerStatus.includes('running')) {
                 actionButtons += `
-                    <button class="btn btn-sm btn-outline-warning mb-1 mr-1 action-stop" data-id="${containerId}">
-                        <i class="fas fa-stop"></i> 停止
-                    </button>
-                    <button class="btn btn-sm btn-outline-primary mb-1 mr-1 action-restart" data-id="${containerId}">
-                        <i class="fas fa-sync-alt"></i> 重启
-                    </button>
+                        <option class="dropdown-item action-stop" data-id="${containerId}">停止容器</option>
                 `;
             } else if (lowerStatus.includes('exited') || lowerStatus.includes('stopped') || lowerStatus.includes('created')) {
                 actionButtons += `
-                    <button class="btn btn-sm btn-outline-success mb-1 mr-1 action-start" data-id="${containerId}">
-                        <i class="fas fa-play"></i> 启动
-                    </button>
-                    <button class="btn btn-sm btn-outline-danger mb-1 mr-1 action-remove" data-id="${containerId}">
-                        <i class="fas fa-trash-alt"></i> 删除
-                    </button>
+                        <option class="dropdown-item action-start" data-id="${containerId}">启动容器</option>
                 `;
             } else if (lowerStatus.includes('paused')) {
                 actionButtons += `
-                    <button class="btn btn-sm btn-outline-success mb-1 mr-1 action-unpause" data-id="${containerId}">
-                        <i class="fas fa-play"></i> 恢复
-                    </button>
+                        <option class="dropdown-item action-unpause" data-id="${containerId}">恢复容器</option>
                 `;
             }
             
-            // 更新容器按钮(总是显示)
+            // 重启和删除操作对所有状态都可用
             actionButtons += `
-                <button class="btn btn-sm btn-outline-primary mb-1 mr-1 action-update" data-id="${containerId}" data-image="${containerImage || ''}">
-                    <i class="fas fa-cloud-download-alt"></i> 更新
-                </button>
+                        <option class="dropdown-item action-restart" data-id="${containerId}">重启容器</option>
+                        <option class="dropdown-item action-stop" data-id="${containerId}">停止容器</option>
+                        <option class="dropdown-item action-remove" data-id="${containerId}">删除容器</option>
+                        <option class="dropdown-item action-update" data-id="${containerId}" data-image="${containerImage || ''}">更新容器</option>
+                    </select>
+                </div>
             `;
 
             html += `
                 <tr>
-                    <td data-label="ID" title="${containerId}">${containerId.substring(0, 12)}</td>
-                    <td data-label="名称" title="${containerName}">${containerName}</td>
-                    <td data-label="镜像" title="${containerImage}">${containerImage}</td>
-                    <td data-label="状态"><span class="badge ${statusClass}">${status}</span></td>
-                    <td data-label="操作" class="action-cell">
+                    <td data-label="ID" title="${containerId}" class="text-center">${containerId.substring(0, 12)}</td>
+                    <td data-label="名称" title="${containerName}" class="text-center">${containerName}</td>
+                    <td data-label="镜像" title="${containerImage}" class="text-center">${containerImage}</td>
+                    <td data-label="状态" class="text-center"><span class="badge ${statusClass}">${status}</span></td>
+                    <td data-label="操作" class="action-cell text-center">
                         <div class="action-buttons">
                             ${actionButtons}
                         </div>
@@ -265,54 +529,90 @@ const dockerManager = {
         
         // 为所有操作按钮绑定事件
         this.setupButtonListeners();
+        
+        // 确保在内容渲染后立即初始化下拉菜单
+        setTimeout(() => {
+            this.initDropdowns();
+            // 备用方法:直接为下拉菜单按钮添加点击事件
+            this.setupManualDropdowns();
+        }, 100); // 增加延迟确保DOM完全渲染
     },
     
-    // 为所有操作按钮绑定事件
+    // 为所有操作按钮绑定事件 - 简化此方法,专注于直接点击处理
     setupButtonListeners() {
-        // 查找所有操作按钮并绑定点击事件
-        document.querySelectorAll('.action-cell button').forEach(button => {
-            const action = Array.from(button.classList).find(cls => cls.startsWith('action-'));
-            if (!action) return;
-            
-            const containerId = button.dataset.id;
-            if (!containerId) return;
-            
-            button.addEventListener('click', (event) => {
+        // 为下拉框选择事件添加处理逻辑
+        document.querySelectorAll('.action-cell .simple-dropdown').forEach(select => {
+            select.addEventListener('change', (event) => {
                 event.preventDefault();
-                const containerName = button.dataset.name;
-                const containerImage = button.dataset.image;
                 
-                switch (action) {
-                    case 'action-logs':
-                        this.showContainerLogs(containerId, containerName);
-                        break;
-                    case 'action-details':
-                        this.showContainerDetails(containerId);
-                        break;
-                    case 'action-stop':
-                        this.stopContainer(containerId);
-                        break;
-                    case 'action-start':
-                        this.startContainer(containerId);
-                        break;
-                    case 'action-restart':
-                        this.restartContainer(containerId);
-                        break;
-                    case 'action-remove':
-                        this.removeContainer(containerId);
-                        break;
-                    case 'action-unpause':
-                        // this.unpauseContainer(containerId); // 假设有这个函数
-                        console.warn('Unpause action not implemented yet.');
-                        break;
-                    case 'action-update':
-                        this.updateContainer(containerId, containerImage);
-                        break;
-                    default:
-                        console.warn('Unknown action:', action);
-                }
+                const selectedOption = select.options[select.selectedIndex];
+                if (!selectedOption || selectedOption.disabled) return;
+                
+                const action = Array.from(selectedOption.classList).find(cls => cls.startsWith('action-'));
+                if (!action) return;
+                
+                const containerId = selectedOption.getAttribute('data-id');
+                if (!containerId) return;
+                
+                const containerName = selectedOption.getAttribute('data-name');
+                const containerImage = selectedOption.getAttribute('data-image');
+                
+                console.log('处理容器操作:', action, '容器ID:', containerId);
+                
+                // 执行对应的容器操作
+                this.handleContainerAction(action, containerId, containerName, containerImage);
+                
+                // 重置选择,以便下次可以再次选择相同选项
+                select.selectedIndex = 0;
             });
         });
+        
+        // 让下拉框按钮隐藏,只显示select元素
+        document.querySelectorAll('.simple-dropdown-toggle').forEach(button => {
+            button.style.display = 'none';
+        });
+        
+        // 样式化select元素
+        document.querySelectorAll('.simple-dropdown').forEach(select => {
+            select.style.display = 'block';
+            select.style.width = '100%';
+            select.style.padding = '0.375rem 0.75rem';
+            select.style.fontSize = '0.875rem';
+            select.style.borderRadius = '0.25rem';
+            select.style.border = '1px solid #ced4da';
+            select.style.backgroundColor = '#fff';
+        });
+    },
+    
+    // 处理容器操作的统一方法
+    handleContainerAction(action, containerId, containerName, containerImage) {
+        console.log('Handling container action:', action, 'for container:', containerId);
+        
+        switch (action) {
+            case 'action-logs':
+                this.showContainerLogs(containerId, containerName);
+                break;
+            case 'action-stop':
+                this.stopContainer(containerId);
+                break;
+            case 'action-start':
+                this.startContainer(containerId);
+                break;
+            case 'action-restart':
+                this.restartContainer(containerId);
+                break;
+            case 'action-remove':
+                this.removeContainer(containerId);
+                break;
+            case 'action-unpause':
+                console.warn('Unpause action not implemented yet.');
+                break;
+            case 'action-update':
+                this.updateContainer(containerId, containerImage);
+                break;
+            default:
+                console.warn('Unknown action:', action);
+        }
     },
     
     // 获取容器状态对应的 CSS 类 - 保持
@@ -326,13 +626,14 @@ const dockerManager = {
         return 'status-unknown';
     },
     
-    // 设置下拉菜单动作的事件监听 (委托方法 - 现在直接使用按钮,不再需要)
+    // 设置下拉菜单动作的事件监听 - 简化为空方法,因为使用原生select不需要
     setupActionDropdownListener() {
-        // 这个方法留作兼容性,但实际上我们现在直接使用按钮而非下拉菜单
+        // 不需要特殊处理,使用原生select元素的change事件
     },
 
-    // 查看日志 (示例:用 SweetAlert 显示)
+    // 查看日志
     async showContainerLogs(containerId, containerName) {
+        console.log('正在获取日志,容器ID:', containerId, '容器名称:', containerName);
         core.showLoading('正在加载日志...');
         try {
             // 注意: 后端 /api/docker/containers/:id/logs 需要存在并返回日志文本
@@ -357,7 +658,7 @@ const dockerManager = {
         } catch (error) {
             core.hideLoading();
             core.showAlert(`查看日志失败: ${error.message}`, 'error');
-            logger.error(`[dockerManager] Error fetching logs for ${containerId}:`, error);
+            console.error(`[dockerManager] Error fetching logs for ${containerId}:`, error);
         }
     },
     
@@ -423,6 +724,11 @@ const dockerManager = {
             }
             core.showAlert(data.message || '容器停止成功', 'success');
             systemStatus.refreshSystemStatus(); // 刷新整体状态
+            
+            // 刷新已停止容器列表
+            if (window.app && typeof window.app.refreshStoppedContainers === 'function') {
+                window.app.refreshStoppedContainers();
+            }
         } catch (error) {
             core.hideLoading();
             core.showAlert(`停止容器失败: ${error.message}`, 'error');
@@ -491,6 +797,7 @@ const dockerManager = {
             cancelButtonText: '取消',
             confirmButtonColor: '#3085d6',
             cancelButtonColor: '#d33',
+            width: '36em', // 增加弹窗宽度
             inputValidator: (value) => {
                 if (!value || value.trim() === '') {
                     return '镜像标签不能为空!';
@@ -511,6 +818,37 @@ const dockerManager = {
                 confirmButton: 'update-confirm',
                 cancelButton: 'update-cancel',
                 footer: 'update-footer'
+            },
+            // 添加自定义CSS
+            didOpen: () => {
+                // 修复输入框宽度
+                const inputElement = Swal.getInput();
+                if (inputElement) {
+                    inputElement.style.maxWidth = '100%';
+                    inputElement.style.width = '100%';
+                    inputElement.style.boxSizing = 'border-box';
+                    inputElement.style.margin = '0';
+                    inputElement.style.padding = '0.5rem';
+                }
+                
+                // 修复输入标签宽度
+                const inputLabel = Swal.getPopup().querySelector('.swal2-input-label');
+                if (inputLabel) {
+                    inputLabel.style.whiteSpace = 'normal';
+                    inputLabel.style.textAlign = 'left';
+                    inputLabel.style.width = '100%';
+                    inputLabel.style.padding = '0 10px';
+                    inputLabel.style.boxSizing = 'border-box';
+                    inputLabel.style.marginBottom = '0.5rem';
+                }
+                
+                // 调整弹窗内容区域
+                const content = Swal.getPopup().querySelector('.swal2-content');
+                if (content) {
+                    content.style.padding = '0 1.5rem';
+                    content.style.boxSizing = 'border-box';
+                    content.style.width = '100%';
+                }
             }
         });
 
@@ -670,6 +1008,58 @@ const dockerManager = {
                 console.warn('[dockerManager] Troubleshoot button not found for binding.');
             }
         }, 0); // 延迟 0ms 执行,让浏览器有机会渲染
+    },
+
+    // 新增方法: 应用表格样式
+    applyTableStyles(table) {
+        if (!table) return;
+        
+        // 添加基本样式
+        table.style.width = "100%";
+        table.style.tableLayout = "auto";
+        table.style.borderCollapse = "collapse";
+        
+        // 设置表头样式
+        const thead = table.querySelector('thead');
+        if (thead) {
+            thead.style.backgroundColor = "#f8f9fa";
+            thead.style.fontWeight = "bold";
+            const thCells = thead.querySelectorAll('th');
+            thCells.forEach(th => {
+                th.style.textAlign = "center";
+                th.style.padding = "10px 8px";
+                th.style.verticalAlign = "middle";
+            });
+        }
+        
+        // 添加响应式样式
+        const style = document.createElement('style');
+        style.textContent = `
+            #dockerStatusTable {
+                width: 100%;
+                table-layout: auto;
+            }
+            #dockerStatusTable th, #dockerStatusTable td {
+                text-align: center;
+                vertical-align: middle;
+                padding: 8px;
+            }
+            #dockerStatusTable td.action-cell {
+                padding: 4px;
+            }
+            @media (max-width: 768px) {
+                #dockerStatusTable {
+                    table-layout: fixed;
+                }
+            }
+        `;
+        
+        // 检查是否已经添加了样式
+        const existingStyle = document.querySelector('style[data-for="dockerStatusTable"]');
+        if (!existingStyle) {
+            style.setAttribute('data-for', 'dockerStatusTable');
+            document.head.appendChild(style);
+        }
     }
 };
 

+ 5 - 0
hubcmdui/web/js/systemStatus.js

@@ -143,6 +143,11 @@ async function refreshSystemStatus() {
                 timer: 3000
             });
         }
+
+        // 刷新已停止容器列表
+        if (window.app && typeof window.app.refreshStoppedContainers === 'function') {
+            window.app.refreshStoppedContainers();
+        }
     } catch (error) {
         // logger.error('刷新系统状态出错:', error);
         showSystemStatusError(error.message);

+ 79 - 7
hubcmdui/web/js/userCenter.js

@@ -167,14 +167,18 @@ function isPasswordComplex(password) {
 function checkUcPasswordStrength() {
     const password = document.getElementById('ucNewPassword').value;
     const strengthSpan = document.getElementById('ucPasswordStrength');
+    const strengthBar = document.getElementById('strengthBar');
     
     if (!password) {
         strengthSpan.textContent = '';
+        if (strengthBar) strengthBar.style.width = '0%';
         return;
     }
     
     let strength = 0;
     let strengthText = '';
+    let strengthColor = '';
+    let strengthWidth = '0%';
     
     // 长度检查
     if (password.length >= 8) strength++;
@@ -194,27 +198,93 @@ function checkUcPasswordStrength() {
         case 0:
         case 1:
             strengthText = '密码强度:非常弱';
-            strengthSpan.style.color = '#FF4136';
+            strengthColor = '#FF4136';
+            strengthWidth = '20%';
             break;
         case 2:
             strengthText = '密码强度:弱';
-            strengthSpan.style.color = '#FF851B';
+            strengthColor = '#FF851B';
+            strengthWidth = '40%';
             break;
         case 3:
             strengthText = '密码强度:中';
-            strengthSpan.style.color = '#FFDC00';
+            strengthColor = '#FFDC00';
+            strengthWidth = '60%';
             break;
         case 4:
             strengthText = '密码强度:强';
-            strengthSpan.style.color = '#2ECC40';
+            strengthColor = '#2ECC40';
+            strengthWidth = '80%';
             break;
         case 5:
             strengthText = '密码强度:非常强';
-            strengthSpan.style.color = '#3D9970';
+            strengthColor = '#3D9970';
+            strengthWidth = '100%';
             break;
     }
     
-    strengthSpan.textContent = strengthText;
+    // 用span元素包裹文本,并设置为不换行
+    strengthSpan.innerHTML = `<span style="white-space: nowrap;">${strengthText}</span>`;
+    strengthSpan.style.color = strengthColor;
+    
+    if (strengthBar) {
+        strengthBar.style.width = strengthWidth;
+        strengthBar.style.backgroundColor = strengthColor;
+    }
+}
+
+// 切换密码可见性
+function togglePasswordVisibility(inputId) {
+    const passwordInput = document.getElementById(inputId);
+    const toggleBtn = passwordInput.nextElementSibling.querySelector('i');
+    
+    if (passwordInput.type === 'password') {
+        passwordInput.type = 'text';
+        toggleBtn.classList.remove('fa-eye');
+        toggleBtn.classList.add('fa-eye-slash');
+    } else {
+        passwordInput.type = 'password';
+        toggleBtn.classList.remove('fa-eye-slash');
+        toggleBtn.classList.add('fa-eye');
+    }
+}
+
+// 刷新用户信息
+function refreshUserInfo() {
+    // 显示刷新动画
+    Swal.fire({
+        title: '刷新中...',
+        html: '<i class="fas fa-sync-alt fa-spin"></i> 正在刷新用户信息',
+        showConfirmButton: false,
+        allowOutsideClick: false,
+        timer: 1500
+    });
+    
+    // 调用获取用户信息
+    getUserInfo().then(() => {
+        // 更新页面上的用户名称
+        const usernameElement = document.getElementById('profileUsername');
+        const currentUsername = document.getElementById('currentUsername');
+        if (usernameElement && currentUsername) {
+            usernameElement.textContent = currentUsername.textContent || '管理员';
+        }
+        
+        // 显示成功消息
+        Swal.fire({
+            title: '刷新成功',
+            icon: 'success',
+            timer: 1500,
+            showConfirmButton: false
+        });
+    }).catch(error => {
+        Swal.fire({
+            title: '刷新失败',
+            text: error.message || '无法获取最新用户信息',
+            icon: 'error',
+            timer: 2000,
+            showConfirmButton: false
+        });
+    });
 }
 
 // 初始化用户中心
@@ -250,7 +320,9 @@ const userCenter = {
     checkUcPasswordStrength,
     initUserCenter,
     loadUserStats,
-    isPasswordComplex
+    isPasswordComplex,
+    togglePasswordVisibility,
+    refreshUserInfo
 };
 
 // 页面加载完成后初始化