From 08ab90382e4a613c8f173b11cafe2fbad8e1c5ce Mon Sep 17 00:00:00 2001 From: dqzboy Date: Sat, 10 May 2025 19:42:04 +0800 Subject: [PATCH] fix: Fix login error, update container management operations, and update user center operation page. --- hubcmdui/compatibility-layer.js | 61 +- hubcmdui/data/config.json | 2 +- hubcmdui/documentation/1743542841590.json | 2 +- hubcmdui/documentation/1743543376091.json | 2 +- hubcmdui/routes/monitoring.js | 5 +- hubcmdui/server.js | 2 +- hubcmdui/services/dockerService.js | 65 +- hubcmdui/users.json | 4 +- hubcmdui/web/admin.html | 722 ++++++++++++++++++++-- hubcmdui/web/js/app.js | 85 ++- hubcmdui/web/js/core.js | 3 +- hubcmdui/web/js/dockerManager.js | 598 ++++++++++++++---- hubcmdui/web/js/systemStatus.js | 5 + hubcmdui/web/js/userCenter.js | 86 ++- 14 files changed, 1424 insertions(+), 218 deletions(-) diff --git a/hubcmdui/compatibility-layer.js b/hubcmdui/compatibility-layer.js index 5516741..ea754f6 100644 --- a/hubcmdui/compatibility-layer.js +++ b/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 { diff --git a/hubcmdui/data/config.json b/hubcmdui/data/config.json index ae3cda1..968d10a 100644 --- a/hubcmdui/data/config.json +++ b/hubcmdui/data/config.json @@ -25,5 +25,5 @@ "monitorInterval": 60, "isEnabled": false }, - "proxyDomain": "dqzboy.github.io" + "proxyDomain": "github.dqzboy.Docker-Proxy" } \ No newline at end of file diff --git a/hubcmdui/documentation/1743542841590.json b/hubcmdui/documentation/1743542841590.json index cf5d7bb..2a9b100 100644 --- a/hubcmdui/documentation/1743542841590.json +++ b/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" } \ No newline at end of file diff --git a/hubcmdui/documentation/1743543376091.json b/hubcmdui/documentation/1743543376091.json index 1318017..43907e5 100644 --- a/hubcmdui/documentation/1743543376091.json +++ b/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" } \ No newline at end of file diff --git a/hubcmdui/routes/monitoring.js b/hubcmdui/routes/monitoring.js index 01aa952..cc679dc 100644 --- a/hubcmdui/routes/monitoring.js +++ b/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(' ') }; }); diff --git a/hubcmdui/server.js b/hubcmdui/server.js index 3089ac4..23f93d5 100644 --- a/hubcmdui/server.js +++ b/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}`); diff --git a/hubcmdui/services/dockerService.js b/hubcmdui/services/dockerService.js index c37ea84..c678f27 100644 --- a/hubcmdui/services/dockerService.js +++ b/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, diff --git a/hubcmdui/users.json b/hubcmdui/users.json index 2dbc255..3da8a86 100644 --- a/hubcmdui/users.json +++ b/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" } ] } \ No newline at end of file diff --git a/hubcmdui/web/admin.html b/hubcmdui/web/admin.html index 370a24e..c938e1a 100644 --- a/hubcmdui/web/admin.html +++ b/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替代 */ + } @@ -1856,13 +2464,6 @@
- -
-

最近容器操作

- - -
-
@@ -1873,13 +2474,13 @@ - +
- + @@ -2046,9 +2647,10 @@ - - - + + + + @@ -2066,40 +2668,67 @@ -
-
-

账户信息

-
-
-
--
-
登录次数
-
-
-
--
-
上次登录
-
-
-
--
-
账户天数
+ + + +
+ +
+
+

账户信息

+
+
+
+
--
+
登录次数
+
+
+
+
--
+
上次登录
+
+
+
+
--
+
账户天数
+
- -
-
-

修改密码

-
- - - - 密码必须包含至少一个字母、一个数字和一个特殊字符,长度在8到16个字符之间 - - - - - - + +
+
+

修改密码

+
+ + + + 密码必须包含至少一个字母、一个数字和一个特殊字符,长度在8到16个字符之间 + + + +
+ + +
+ +
@@ -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未定义'); + }; }); diff --git a/hubcmdui/web/js/app.js b/hubcmdui/web/js/app.js index 40a42a9..a001fa5 100644 --- a/hubcmdui/web/js/app.js +++ b/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 = '
'; + tbody.innerHTML = ''; return; } containers.forEach(container => { + // 调试单个容器数据 + console.log('容器数据:', container.id, container.name, + '镜像:', container.image, + '状态:', container.status); + const row = ` + `; @@ -416,7 +456,7 @@ function refreshStoppedContainers() { .catch(error => { console.error('获取已停止容器列表失败:', error); document.getElementById('stoppedContainersBody').innerHTML = - ''; + ''; }); } @@ -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 }; diff --git a/hubcmdui/web/js/core.js b/hubcmdui/web/js/core.js index f3765fb..7ea9e61 100644 --- a/hubcmdui/web/js/core.js +++ b/hubcmdui/web/js/core.js @@ -516,5 +516,6 @@ window.core = { formatDateTime, debounce, throttle, - toggleLoadingState + toggleLoadingState, + initEventListeners }; diff --git a/hubcmdui/web/js/dockerManager.js b/hubcmdui/web/js/dockerManager.js index 69ebeb2..14999c1 100644 --- a/hubcmdui/web/js/dockerManager.js +++ b/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 = ` - - - - - + + + + + `; } @@ -127,14 +262,151 @@ const dockerManager = { `; + + // 添加表格样式 + this.applyTableStyles(table); } }, + // 新增:显示刷新中状态 + showRefreshingState(refreshBtn) { + if (!refreshBtn) return; + + // 保存原始按钮内容 + const originalContent = refreshBtn.innerHTML; + + // 更改为加载状态 + refreshBtn.innerHTML = ' 刷新中...'; + 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 = ` +
+

正在更新容器列表...

+ `; + + // 获取表格的位置并设置覆盖层 + 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 调用 renderContainersTable(containers, dockerStatus) { // 减少详细日志输出 // 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 = ' 刷新列表'; + 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 = ` - - - - - + + + + + `; @@ -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 += ` - - + // 创建按钮组,使用标准Bootstrap 5下拉菜单语法 + let actionButtons = ` +
+ + +
`; html += ` - - - - - + + + +
容器 ID名称状态容器ID容器名称镜像名称运行状态
没有已停止的容器
没有已停止的容器
${container.id} ${container.name}${container.image ? container.image : '未知'} ${container.status}
获取已停止容器列表失败
获取已停止容器列表失败
容器ID容器名称镜像名称运行状态操作容器ID容器名称镜像名称运行状态操作
容器ID容器名称镜像名称运行状态操作容器ID容器名称镜像名称运行状态操作
${containerId.substring(0, 12)}${containerName}${containerImage}${status} + ${containerId.substring(0, 12)}${containerName}${containerImage}${status}
${actionButtons}
@@ -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); + } } }; diff --git a/hubcmdui/web/js/systemStatus.js b/hubcmdui/web/js/systemStatus.js index abfe99c..08416df 100644 --- a/hubcmdui/web/js/systemStatus.js +++ b/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); diff --git a/hubcmdui/web/js/userCenter.js b/hubcmdui/web/js/userCenter.js index 1bd38eb..8a8d18a 100644 --- a/hubcmdui/web/js/userCenter.js +++ b/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 = `${strengthText}`; + 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: ' 正在刷新用户信息', + 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 }; // 页面加载完成后初始化