mirror of
https://github.com/dqzboy/Docker-Proxy.git
synced 2026-01-12 16:25:42 +08:00
fix: Fix login error, update container management operations, and update user center operation page.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
"monitorInterval": 60,
|
||||
"isEnabled": false
|
||||
},
|
||||
"proxyDomain": "dqzboy.github.io"
|
||||
"proxyDomain": "github.dqzboy.Docker-Proxy"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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(' ')
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -516,5 +516,6 @@ window.core = {
|
||||
formatDateTime,
|
||||
debounce,
|
||||
throttle,
|
||||
toggleLoadingState
|
||||
toggleLoadingState,
|
||||
initEventListeners
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
// 页面加载完成后初始化
|
||||
|
||||
Reference in New Issue
Block a user