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 @@| 容器 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} | +@@ -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 }; // 页面加载完成后初始化 |