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

This commit is contained in:
dqzboy
2025-05-10 19:42:04 +08:00
parent 0d4bd10afe
commit 08ab90382e
14 changed files with 1424 additions and 218 deletions

View File

@@ -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 {

View File

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

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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(' ')
};
});

View File

@@ -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}`);

View File

@@ -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 守护进程');
}
try {
logger.info('正在获取已停止的容器...');
const containers = await docker.listContainers({
all: true,
filters: { status: ['exited', 'dead', 'created'] }
});
return containers.map(container => ({
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,

View File

@@ -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"
}
]
}

View File

@@ -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>
@@ -2047,8 +2648,9 @@
<thead>
<tr>
<th>容器ID</th>
<th>名称</th>
<th>状态</th>
<th>容器名称</th>
<th>镜像名称</th>
<th>运行状态</th>
</tr>
</thead>
<tbody id="stoppedContainersBody"></tbody>
@@ -2066,27 +2668,51 @@
<button class="btn btn-primary" id="ucLogoutBtn" style="display: inline-block;">退出登录</button>
</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-card">
<div class="user-center-section">
<h2 class="user-center-section-title">修改密码</h2>
<form id="changePasswordForm">
@@ -2097,12 +2723,15 @@
<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>
<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>
</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>

View File

@@ -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
};

View File

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

View File

@@ -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,14 +262,151 @@ 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 调用
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 = '<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 服务状态
@@ -199,60 +474,49 @@ 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> 详情
// 创建按钮组使用标准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,30 +529,69 @@ 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-'));
// 为下拉框选择事件添加处理逻辑
document.querySelectorAll('.action-cell .simple-dropdown').forEach(select => {
select.addEventListener('change', (event) => {
event.preventDefault();
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 = button.dataset.id;
const containerId = selectedOption.getAttribute('data-id');
if (!containerId) return;
button.addEventListener('click', (event) => {
event.preventDefault();
const containerName = button.dataset.name;
const containerImage = button.dataset.image;
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-details':
this.showContainerDetails(containerId);
break;
case 'action-stop':
this.stopContainer(containerId);
break;
@@ -302,7 +605,6 @@ const dockerManager = {
this.removeContainer(containerId);
break;
case 'action-unpause':
// this.unpauseContainer(containerId); // 假设有这个函数
console.warn('Unpause action not implemented yet.');
break;
case 'action-update':
@@ -311,8 +613,6 @@ const dockerManager = {
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);
}
}
};

View File

@@ -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);

View File

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