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 {
|
try {
|
||||||
const monitoringService = require('./services/monitoringService');
|
logger.info('兼容层处理获取已停止容器请求');
|
||||||
const stoppedContainers = await monitoringService.getStoppedContainers();
|
const { exec } = require('child_process');
|
||||||
res.json(stoppedContainers);
|
const util = require('util');
|
||||||
} catch (error) {
|
const execPromise = util.promisify(exec);
|
||||||
logger.error('获取已停止容器列表失败:', error);
|
|
||||||
res.status(500).json({ error: '获取已停止容器列表失败', details: error.message });
|
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) => {
|
app.get('/api/system-status', requireLogin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -25,5 +25,5 @@
|
|||||||
"monitorInterval": 60,
|
"monitorInterval": 60,
|
||||||
"isEnabled": false
|
"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```",
|
"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,
|
"published": true,
|
||||||
"createdAt": "2025-04-01T21:27:21.591Z",
|
"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```",
|
"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,
|
"published": true,
|
||||||
"createdAt": "2025-04-01T21:36:16.092Z",
|
"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 util = require('util');
|
||||||
const execPromise = util.promisify(exec);
|
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')
|
const containers = stdout.trim().split('\n')
|
||||||
.filter(line => line.trim())
|
.filter(line => line.trim())
|
||||||
.map(line => {
|
.map(line => {
|
||||||
const [id, name, ...statusParts] = line.split('\t');
|
const [id, name, image, ...statusParts] = line.split('\t');
|
||||||
return {
|
return {
|
||||||
id: id.substring(0, 12),
|
id: id.substring(0, 12),
|
||||||
name,
|
name,
|
||||||
|
image,
|
||||||
status: statusParts.join(' ')
|
status: statusParts.join(' ')
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const compatibilityLayer = require('./compatibility-layer');
|
|||||||
const initSystem = require('./scripts/init-system');
|
const initSystem = require('./scripts/init-system');
|
||||||
|
|
||||||
// 设置日志级别 (默认INFO, 可通过环境变量设置)
|
// 设置日志级别 (默认INFO, 可通过环境变量设置)
|
||||||
const logLevel = process.env.LOG_LEVEL || 'INFO';
|
const logLevel = process.env.LOG_LEVEL || 'WARN';
|
||||||
logger.setLogLevel(logLevel);
|
logger.setLogLevel(logLevel);
|
||||||
logger.info(`日志级别已设置为: ${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) {
|
async function deleteContainer(id) {
|
||||||
const docker = await getDockerConnection();
|
const docker = await getDockerConnection();
|
||||||
@@ -387,16 +414,33 @@ async function getStoppedContainers() {
|
|||||||
throw new Error('无法连接到 Docker 守护进程');
|
throw new Error('无法连接到 Docker 守护进程');
|
||||||
}
|
}
|
||||||
|
|
||||||
const containers = await docker.listContainers({
|
try {
|
||||||
all: true,
|
logger.info('正在获取已停止的容器...');
|
||||||
filters: { status: ['exited', 'dead', 'created'] }
|
const containers = await docker.listContainers({
|
||||||
});
|
all: true,
|
||||||
|
filters: { status: ['exited', 'dead', 'created'] }
|
||||||
return containers.map(container => ({
|
});
|
||||||
id: container.Id.slice(0, 12),
|
|
||||||
name: container.Names[0].replace(/^\//, ''),
|
logger.info(`找到 ${containers.length} 个已停止的容器`);
|
||||||
status: container.State
|
|
||||||
}));
|
// 记录每个容器的信息
|
||||||
|
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事件
|
// 获取最近的Docker事件
|
||||||
@@ -468,6 +512,7 @@ module.exports = {
|
|||||||
getContainerStatus,
|
getContainerStatus,
|
||||||
restartContainer,
|
restartContainer,
|
||||||
stopContainer,
|
stopContainer,
|
||||||
|
startContainer,
|
||||||
deleteContainer,
|
deleteContainer,
|
||||||
updateContainer,
|
updateContainer,
|
||||||
getContainerLogs,
|
getContainerLogs,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"username": "root",
|
"username": "root",
|
||||||
"password": "$2b$10$lh1kqJtq3shL2BhMD1LbVOThGeAlPXsDgME/he4ZyDMRupVtj0Hl.",
|
"password": "$2b$10$HBYJPwEB1gdRxcc6Bm1mKukxCC8eyJOZC7sGJN5meghvsBfoQjKtW",
|
||||||
"loginCount": 0,
|
"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);
|
box-shadow: var(--shadow-md);
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-center-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-center-section {
|
.user-center-section {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-center-section-title {
|
.user-center-section-title {
|
||||||
@@ -350,7 +356,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
@@ -360,6 +366,19 @@
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
text-align: center;
|
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 {
|
.stat-value {
|
||||||
@@ -374,6 +393,307 @@
|
|||||||
font-size: 0.9rem;
|
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 {
|
.sidebar h2 {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
@@ -1784,6 +2104,294 @@
|
|||||||
.swal2-html-container div {
|
.swal2-html-container div {
|
||||||
text-align: center !important;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -1856,13 +2464,6 @@
|
|||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
<!-- 仪表板卡片将由 systemStatus.initDashboard() 动态生成 -->
|
<!-- 仪表板卡片将由 systemStatus.initDashboard() 动态生成 -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-card">
|
|
||||||
<h3 class="card-title">最近容器操作</h3>
|
|
||||||
<table id="recentActivitiesTable">
|
|
||||||
<!-- 活动表内容将由 systemStatus.refreshSystemStatus() 动态更新 -->
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 基本配置 -->
|
<!-- 基本配置 -->
|
||||||
@@ -1873,13 +2474,13 @@
|
|||||||
<label for="logoUrl">Logo URL: (可选)</label>
|
<label for="logoUrl">Logo URL: (可选)</label>
|
||||||
<input type="url" id="logoUrl" name="logoUrl" class="form-control">
|
<input type="url" id="logoUrl" name="logoUrl" class="form-control">
|
||||||
</div>
|
</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;">
|
<div class="form-group" style="margin-top: 2rem;">
|
||||||
<label for="proxyDomain">Docker镜像代理地址: (必填)</label>
|
<label for="proxyDomain">Docker镜像代理地址: (必填)</label>
|
||||||
<input type="text" id="proxyDomain" name="proxyDomain" class="form-control" required>
|
<input type="text" id="proxyDomain" name="proxyDomain" class="form-control" required>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2046,9 +2647,10 @@
|
|||||||
<table id="stoppedContainersTable" class="container-table">
|
<table id="stoppedContainersTable" class="container-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>容器 ID</th>
|
<th>容器ID</th>
|
||||||
<th>名称</th>
|
<th>容器名称</th>
|
||||||
<th>状态</th>
|
<th>镜像名称</th>
|
||||||
|
<th>运行状态</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="stoppedContainersBody"></tbody>
|
<tbody id="stoppedContainersBody"></tbody>
|
||||||
@@ -2066,40 +2668,67 @@
|
|||||||
<button class="btn btn-primary" id="ucLogoutBtn" style="display: inline-block;">退出登录</button>
|
<button class="btn btn-primary" id="ucLogoutBtn" style="display: inline-block;">退出登录</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-center-card">
|
<!-- 个人资料卡片 -->
|
||||||
<div class="user-center-section">
|
<div class="user-profile-card">
|
||||||
<h2 class="user-center-section-title">账户信息</h2>
|
<div class="user-profile-avatar">
|
||||||
<div class="user-stats">
|
<i class="fas fa-user-circle"></i>
|
||||||
<div class="stat-card">
|
</div>
|
||||||
<div class="stat-value" id="loginCount">--</div>
|
<div class="user-profile-info">
|
||||||
<div class="stat-label">登录次数</div>
|
<h2 class="user-profile-name" id="profileUsername">管理员</h2>
|
||||||
</div>
|
<p class="user-profile-role">系统管理员</p>
|
||||||
<div class="stat-card">
|
<div class="user-profile-badges">
|
||||||
<div class="stat-value" id="lastLogin">--</div>
|
<span class="user-badge admin"><i class="fas fa-shield-alt"></i> 管理员</span>
|
||||||
<div class="stat-label">上次登录</div>
|
<span class="user-badge active"><i class="fas fa-check-circle"></i> 活跃</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
</div>
|
||||||
<div class="stat-value" id="accountAge">--</div>
|
<div class="user-profile-actions">
|
||||||
<div class="stat-label">账户天数</div>
|
<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>
|
||||||
|
|
||||||
<!-- 移除用户详细信息部分 -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-center-section">
|
<!-- 密码修改卡片 -->
|
||||||
<h2 class="user-center-section-title">修改密码</h2>
|
<div class="user-center-card">
|
||||||
<form id="changePasswordForm">
|
<div class="user-center-section">
|
||||||
<label for="ucCurrentPassword">当前密码</label>
|
<h2 class="user-center-section-title">修改密码</h2>
|
||||||
<input type="password" id="ucCurrentPassword" name="currentPassword">
|
<form id="changePasswordForm">
|
||||||
<label for="ucNewPassword">新密码</label>
|
<label for="ucCurrentPassword">当前密码</label>
|
||||||
<span class="password-hint" id="ucPasswordHint">密码必须包含至少一个字母、一个数字和一个特殊字符,长度在8到16个字符之间</span>
|
<input type="password" id="ucCurrentPassword" name="currentPassword">
|
||||||
<input type="password" id="ucNewPassword" name="newPassword" oninput="userCenter.checkUcPasswordStrength()">
|
<label for="ucNewPassword">新密码</label>
|
||||||
<label for="ucConfirmPassword">确认新密码</label>
|
<span class="password-hint" id="ucPasswordHint">密码必须包含至少一个字母、一个数字和一个特殊字符,长度在8到16个字符之间</span>
|
||||||
<input type="password" id="ucConfirmPassword" name="confirmPassword">
|
<input type="password" id="ucNewPassword" name="newPassword" oninput="userCenter.checkUcPasswordStrength()">
|
||||||
<span id="ucPasswordStrength" style="color: red;"></span>
|
<label for="ucConfirmPassword">确认新密码</label>
|
||||||
<button type="submit" class="btn btn-primary">修改密码</button>
|
<input type="password" id="ucConfirmPassword" name="confirmPassword">
|
||||||
</form>
|
<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>
|
</div>
|
||||||
@@ -2190,6 +2819,11 @@
|
|||||||
window.saveConfig = window.app ? window.app.saveConfig : function(data) {
|
window.saveConfig = window.app ? window.app.saveConfig : function(data) {
|
||||||
console.error('saveConfig未定义');
|
console.error('saveConfig未定义');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确保validateAndSaveConfig可用
|
||||||
|
window.validateAndSaveConfig = window.app ? window.app.validateAndSaveConfig : function(type) {
|
||||||
|
console.error('validateAndSaveConfig未定义');
|
||||||
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ async function initializeModules() {
|
|||||||
// 加载监控配置
|
// 加载监控配置
|
||||||
await loadMonitoringConfig();
|
await loadMonitoringConfig();
|
||||||
|
|
||||||
|
// 加载已停止的容器列表
|
||||||
|
refreshStoppedContainers();
|
||||||
|
|
||||||
// 显示默认页面 - 使用core中的showSection函数
|
// 显示默认页面 - 使用core中的showSection函数
|
||||||
core.showSection('dashboard');
|
core.showSection('dashboard');
|
||||||
|
|
||||||
@@ -141,6 +144,10 @@ function loadMonitoringConfig() {
|
|||||||
document.getElementById('toggleMonitoringBtn').textContent =
|
document.getElementById('toggleMonitoringBtn').textContent =
|
||||||
config.isEnabled ? '禁用监控' : '启用监控';
|
config.isEnabled ? '禁用监控' : '启用监控';
|
||||||
|
|
||||||
|
// 添加通知类型选择变化的监听器
|
||||||
|
const notificationTypeSelect = document.getElementById('notificationType');
|
||||||
|
notificationTypeSelect.addEventListener('change', toggleNotificationFields);
|
||||||
|
|
||||||
// console.log('监控配置加载完成');
|
// console.log('监控配置加载完成');
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -391,22 +398,55 @@ function refreshStoppedContainers() {
|
|||||||
fetch('/api/stopped-containers')
|
fetch('/api/stopped-containers')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) throw new Error('获取已停止容器列表失败');
|
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 => {
|
.then(containers => {
|
||||||
|
// 添加调试信息
|
||||||
|
console.log('已停止的容器数据:', JSON.stringify(containers, null, 2));
|
||||||
|
|
||||||
const tbody = document.getElementById('stoppedContainersBody');
|
const tbody = document.getElementById('stoppedContainersBody');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
if (containers.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
containers.forEach(container => {
|
containers.forEach(container => {
|
||||||
|
// 调试单个容器数据
|
||||||
|
console.log('容器数据:', container.id, container.name,
|
||||||
|
'镜像:', container.image,
|
||||||
|
'状态:', container.status);
|
||||||
|
|
||||||
const row = `
|
const row = `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${container.id}</td>
|
<td>${container.id}</td>
|
||||||
<td>${container.name}</td>
|
<td>${container.name}</td>
|
||||||
|
<td>${container.image ? container.image : '未知'}</td>
|
||||||
<td>${container.status}</td>
|
<td>${container.status}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
@@ -416,7 +456,7 @@ function refreshStoppedContainers() {
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('获取已停止容器列表失败:', error);
|
console.error('获取已停止容器列表失败:', error);
|
||||||
document.getElementById('stoppedContainersBody').innerHTML =
|
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() {
|
function loadBasicConfig() {
|
||||||
fetch('/api/config')
|
fetch('/api/config')
|
||||||
@@ -482,14 +551,14 @@ function loadBasicConfig() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暴露给全局作用域的函数
|
// 导出需要的函数到window.app对象
|
||||||
window.app = {
|
window.app = {
|
||||||
loadMonitoringConfig,
|
|
||||||
loadBasicConfig,
|
|
||||||
toggleNotificationFields,
|
|
||||||
saveMonitoringConfig,
|
saveMonitoringConfig,
|
||||||
testNotification,
|
testNotification,
|
||||||
toggleMonitoring,
|
toggleMonitoring,
|
||||||
|
toggleNotificationFields,
|
||||||
refreshStoppedContainers,
|
refreshStoppedContainers,
|
||||||
saveConfig
|
saveConfig,
|
||||||
|
loadBasicConfig,
|
||||||
|
validateAndSaveConfig
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -516,5 +516,6 @@ window.core = {
|
|||||||
formatDateTime,
|
formatDateTime,
|
||||||
debounce,
|
debounce,
|
||||||
throttle,
|
throttle,
|
||||||
toggleLoadingState
|
toggleLoadingState,
|
||||||
|
initEventListeners
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,37 +29,169 @@ const dockerManager = {
|
|||||||
|
|
||||||
// 初始化Bootstrap下拉菜单组件
|
// 初始化Bootstrap下拉菜单组件
|
||||||
initDropdowns: function() {
|
initDropdowns: function() {
|
||||||
// 减少日志输出
|
|
||||||
// console.log('[dockerManager] Initializing Bootstrap dropdowns...');
|
|
||||||
|
|
||||||
// 直接初始化,不使用setTimeout避免延迟导致的问题
|
|
||||||
try {
|
try {
|
||||||
// 动态初始化所有下拉菜单
|
console.log('[dockerManager] 初始化下拉菜单...');
|
||||||
|
|
||||||
|
// 动态初始化所有下拉菜单按钮
|
||||||
const dropdownElements = document.querySelectorAll('[data-bs-toggle="dropdown"]');
|
const dropdownElements = document.querySelectorAll('[data-bs-toggle="dropdown"]');
|
||||||
|
console.log(`[dockerManager] 找到 ${dropdownElements.length} 个下拉元素`);
|
||||||
|
|
||||||
if (dropdownElements.length === 0) {
|
if (dropdownElements.length === 0) {
|
||||||
return; // 如果没有找到下拉元素,直接返回
|
return; // 如果没有找到下拉元素,直接返回
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.bootstrap && window.bootstrap.Dropdown) {
|
// 尝试使用所有可能的Bootstrap初始化方法
|
||||||
|
if (window.bootstrap && typeof window.bootstrap.Dropdown !== 'undefined') {
|
||||||
|
console.log('[dockerManager] 使用 Bootstrap 5 初始化下拉菜单');
|
||||||
dropdownElements.forEach(el => {
|
dropdownElements.forEach(el => {
|
||||||
try {
|
try {
|
||||||
new window.bootstrap.Dropdown(el);
|
new window.bootstrap.Dropdown(el);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 静默处理错误,不要输出到控制台
|
console.error('Bootstrap 5 下拉菜单初始化错误:', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (typeof $ !== 'undefined' && typeof $.fn.dropdown !== 'undefined') {
|
||||||
|
console.log('[dockerManager] 使用 jQuery Bootstrap 初始化下拉菜单');
|
||||||
|
$(dropdownElements).dropdown();
|
||||||
} else {
|
} else {
|
||||||
console.warn('Bootstrap Dropdown 组件未找到,将尝试使用jQuery初始化');
|
console.warn('[dockerManager] 未找到Bootstrap下拉菜单组件,将使用手动下拉实现');
|
||||||
// 尝试使用jQuery初始化(如果存在)
|
this.setupManualDropdowns();
|
||||||
if (window.jQuery) {
|
|
||||||
window.jQuery('[data-bs-toggle="dropdown"]').dropdown();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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() {
|
showLoadingState() {
|
||||||
const table = document.getElementById('dockerStatusTable');
|
const table = document.getElementById('dockerStatusTable');
|
||||||
@@ -89,6 +221,9 @@ const dockerManager = {
|
|||||||
const refreshBtn = document.getElementById('refreshDockerBtn');
|
const refreshBtn = document.getElementById('refreshDockerBtn');
|
||||||
if (refreshBtn) {
|
if (refreshBtn) {
|
||||||
refreshBtn.addEventListener('click', () => {
|
refreshBtn.addEventListener('click', () => {
|
||||||
|
// 显示加载状态,提高用户体验
|
||||||
|
this.showRefreshingState(refreshBtn);
|
||||||
|
|
||||||
if (window.systemStatus && typeof window.systemStatus.refreshSystemStatus === 'function') {
|
if (window.systemStatus && typeof window.systemStatus.refreshSystemStatus === 'function') {
|
||||||
window.systemStatus.refreshSystemStatus();
|
window.systemStatus.refreshSystemStatus();
|
||||||
}
|
}
|
||||||
@@ -107,11 +242,11 @@ const dockerManager = {
|
|||||||
if (thead) {
|
if (thead) {
|
||||||
thead.innerHTML = `
|
thead.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 120px;">容器ID</th>
|
<th style="width: 12%;">容器ID</th>
|
||||||
<th style="width: 25%;">容器名称</th>
|
<th style="width: 18%;">容器名称</th>
|
||||||
<th style="width: 35%;">镜像名称</th>
|
<th style="width: 30%;">镜像名称</th>
|
||||||
<th style="width: 100px;">运行状态</th>
|
<th style="width: 15%;">运行状态</th>
|
||||||
<th style="width: 150px;">操作</th>
|
<th style="width: 15%;">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -127,14 +262,151 @@ const dockerManager = {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 调用
|
// 渲染容器表格 - 核心渲染函数,由 systemStatus 调用
|
||||||
renderContainersTable(containers, dockerStatus) {
|
renderContainersTable(containers, dockerStatus) {
|
||||||
// 减少详细日志输出
|
// 减少详细日志输出
|
||||||
// console.log(`[dockerManager] Rendering containers table. Containers count: ${containers ? containers.length : 0}`);
|
// 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');
|
const tbody = document.getElementById('dockerStatusTableBody');
|
||||||
if (!tbody) {
|
if (!tbody) {
|
||||||
return;
|
return;
|
||||||
@@ -149,11 +421,11 @@ const dockerManager = {
|
|||||||
const newThead = thead || document.createElement('thead');
|
const newThead = thead || document.createElement('thead');
|
||||||
newThead.innerHTML = `
|
newThead.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 120px;">容器ID</th>
|
<th style="width: 12%;">容器ID</th>
|
||||||
<th style="width: 25%;">容器名称</th>
|
<th style="width: 18%;">容器名称</th>
|
||||||
<th style="width: 35%;">镜像名称</th>
|
<th style="width: 30%;">镜像名称</th>
|
||||||
<th style="width: 100px;">运行状态</th>
|
<th style="width: 15%;">运行状态</th>
|
||||||
<th style="width: 150px;">操作</th>
|
<th style="width: 15%;">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -161,6 +433,9 @@ const dockerManager = {
|
|||||||
table.insertBefore(newThead, tbody);
|
table.insertBefore(newThead, tbody);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 应用表格样式
|
||||||
|
this.applyTableStyles(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 检查 Docker 服务状态
|
// 1. 检查 Docker 服务状态
|
||||||
@@ -198,61 +473,50 @@ const dockerManager = {
|
|||||||
|
|
||||||
// 添加lowerStatus变量定义,修复错误
|
// 添加lowerStatus变量定义,修复错误
|
||||||
const lowerStatus = status.toLowerCase();
|
const lowerStatus = status.toLowerCase();
|
||||||
|
|
||||||
// 替换下拉菜单实现为直接的操作按钮
|
|
||||||
let actionButtons = '';
|
|
||||||
|
|
||||||
// 基本操作:查看日志和详情
|
// 创建按钮组,使用标准Bootstrap 5下拉菜单语法
|
||||||
actionButtons += `
|
let actionButtons = `
|
||||||
<button class="btn btn-sm btn-outline-info mb-1 mr-1 action-logs" data-id="${containerId}" data-name="${containerName}">
|
<div class="btn-group">
|
||||||
<i class="fas fa-file-alt"></i> 日志
|
<button type="button" class="btn btn-sm btn-primary dropdown-toggle simple-dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
</button>
|
操作
|
||||||
<button class="btn btn-sm btn-outline-secondary mb-1 mr-1 action-details" data-id="${containerId}">
|
</button>
|
||||||
<i class="fas fa-info-circle"></i> 详情
|
<select class="simple-dropdown">
|
||||||
</button>
|
<option value="" selected disabled>选择操作</option>
|
||||||
|
<option class="dropdown-item action-logs" data-id="${containerId}" data-name="${containerName}">查看日志</option>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 根据状态显示不同操作
|
// 根据状态添加不同的操作选项
|
||||||
if (lowerStatus.includes('running')) {
|
if (lowerStatus.includes('running')) {
|
||||||
actionButtons += `
|
actionButtons += `
|
||||||
<button class="btn btn-sm btn-outline-warning mb-1 mr-1 action-stop" data-id="${containerId}">
|
<option class="dropdown-item action-stop" data-id="${containerId}">停止容器</option>
|
||||||
<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>
|
|
||||||
`;
|
`;
|
||||||
} else if (lowerStatus.includes('exited') || lowerStatus.includes('stopped') || lowerStatus.includes('created')) {
|
} else if (lowerStatus.includes('exited') || lowerStatus.includes('stopped') || lowerStatus.includes('created')) {
|
||||||
actionButtons += `
|
actionButtons += `
|
||||||
<button class="btn btn-sm btn-outline-success mb-1 mr-1 action-start" data-id="${containerId}">
|
<option class="dropdown-item action-start" data-id="${containerId}">启动容器</option>
|
||||||
<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>
|
|
||||||
`;
|
`;
|
||||||
} else if (lowerStatus.includes('paused')) {
|
} else if (lowerStatus.includes('paused')) {
|
||||||
actionButtons += `
|
actionButtons += `
|
||||||
<button class="btn btn-sm btn-outline-success mb-1 mr-1 action-unpause" data-id="${containerId}">
|
<option class="dropdown-item action-unpause" data-id="${containerId}">恢复容器</option>
|
||||||
<i class="fas fa-play"></i> 恢复
|
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新容器按钮(总是显示)
|
// 重启和删除操作对所有状态都可用
|
||||||
actionButtons += `
|
actionButtons += `
|
||||||
<button class="btn btn-sm btn-outline-primary mb-1 mr-1 action-update" data-id="${containerId}" data-image="${containerImage || ''}">
|
<option class="dropdown-item action-restart" data-id="${containerId}">重启容器</option>
|
||||||
<i class="fas fa-cloud-download-alt"></i> 更新
|
<option class="dropdown-item action-stop" data-id="${containerId}">停止容器</option>
|
||||||
</button>
|
<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 += `
|
html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td data-label="ID" title="${containerId}">${containerId.substring(0, 12)}</td>
|
<td data-label="ID" title="${containerId}" class="text-center">${containerId.substring(0, 12)}</td>
|
||||||
<td data-label="名称" title="${containerName}">${containerName}</td>
|
<td data-label="名称" title="${containerName}" class="text-center">${containerName}</td>
|
||||||
<td data-label="镜像" title="${containerImage}">${containerImage}</td>
|
<td data-label="镜像" title="${containerImage}" class="text-center">${containerImage}</td>
|
||||||
<td data-label="状态"><span class="badge ${statusClass}">${status}</span></td>
|
<td data-label="状态" class="text-center"><span class="badge ${statusClass}">${status}</span></td>
|
||||||
<td data-label="操作" class="action-cell">
|
<td data-label="操作" class="action-cell text-center">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
${actionButtons}
|
${actionButtons}
|
||||||
</div>
|
</div>
|
||||||
@@ -265,54 +529,90 @@ const dockerManager = {
|
|||||||
|
|
||||||
// 为所有操作按钮绑定事件
|
// 为所有操作按钮绑定事件
|
||||||
this.setupButtonListeners();
|
this.setupButtonListeners();
|
||||||
|
|
||||||
|
// 确保在内容渲染后立即初始化下拉菜单
|
||||||
|
setTimeout(() => {
|
||||||
|
this.initDropdowns();
|
||||||
|
// 备用方法:直接为下拉菜单按钮添加点击事件
|
||||||
|
this.setupManualDropdowns();
|
||||||
|
}, 100); // 增加延迟确保DOM完全渲染
|
||||||
},
|
},
|
||||||
|
|
||||||
// 为所有操作按钮绑定事件
|
// 为所有操作按钮绑定事件 - 简化此方法,专注于直接点击处理
|
||||||
setupButtonListeners() {
|
setupButtonListeners() {
|
||||||
// 查找所有操作按钮并绑定点击事件
|
// 为下拉框选择事件添加处理逻辑
|
||||||
document.querySelectorAll('.action-cell button').forEach(button => {
|
document.querySelectorAll('.action-cell .simple-dropdown').forEach(select => {
|
||||||
const action = Array.from(button.classList).find(cls => cls.startsWith('action-'));
|
select.addEventListener('change', (event) => {
|
||||||
if (!action) return;
|
|
||||||
|
|
||||||
const containerId = button.dataset.id;
|
|
||||||
if (!containerId) return;
|
|
||||||
|
|
||||||
button.addEventListener('click', (event) => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const containerName = button.dataset.name;
|
|
||||||
const containerImage = button.dataset.image;
|
|
||||||
|
|
||||||
switch (action) {
|
const selectedOption = select.options[select.selectedIndex];
|
||||||
case 'action-logs':
|
if (!selectedOption || selectedOption.disabled) return;
|
||||||
this.showContainerLogs(containerId, containerName);
|
|
||||||
break;
|
const action = Array.from(selectedOption.classList).find(cls => cls.startsWith('action-'));
|
||||||
case 'action-details':
|
if (!action) return;
|
||||||
this.showContainerDetails(containerId);
|
|
||||||
break;
|
const containerId = selectedOption.getAttribute('data-id');
|
||||||
case 'action-stop':
|
if (!containerId) return;
|
||||||
this.stopContainer(containerId);
|
|
||||||
break;
|
const containerName = selectedOption.getAttribute('data-name');
|
||||||
case 'action-start':
|
const containerImage = selectedOption.getAttribute('data-image');
|
||||||
this.startContainer(containerId);
|
|
||||||
break;
|
console.log('处理容器操作:', action, '容器ID:', containerId);
|
||||||
case 'action-restart':
|
|
||||||
this.restartContainer(containerId);
|
// 执行对应的容器操作
|
||||||
break;
|
this.handleContainerAction(action, containerId, containerName, containerImage);
|
||||||
case 'action-remove':
|
|
||||||
this.removeContainer(containerId);
|
// 重置选择,以便下次可以再次选择相同选项
|
||||||
break;
|
select.selectedIndex = 0;
|
||||||
case 'action-unpause':
|
|
||||||
// this.unpauseContainer(containerId); // 假设有这个函数
|
|
||||||
console.warn('Unpause action not implemented yet.');
|
|
||||||
break;
|
|
||||||
case 'action-update':
|
|
||||||
this.updateContainer(containerId, containerImage);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn('Unknown action:', action);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 让下拉框按钮隐藏,只显示select元素
|
||||||
|
document.querySelectorAll('.simple-dropdown-toggle').forEach(button => {
|
||||||
|
button.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 样式化select元素
|
||||||
|
document.querySelectorAll('.simple-dropdown').forEach(select => {
|
||||||
|
select.style.display = 'block';
|
||||||
|
select.style.width = '100%';
|
||||||
|
select.style.padding = '0.375rem 0.75rem';
|
||||||
|
select.style.fontSize = '0.875rem';
|
||||||
|
select.style.borderRadius = '0.25rem';
|
||||||
|
select.style.border = '1px solid #ced4da';
|
||||||
|
select.style.backgroundColor = '#fff';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理容器操作的统一方法
|
||||||
|
handleContainerAction(action, containerId, containerName, containerImage) {
|
||||||
|
console.log('Handling container action:', action, 'for container:', containerId);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'action-logs':
|
||||||
|
this.showContainerLogs(containerId, containerName);
|
||||||
|
break;
|
||||||
|
case 'action-stop':
|
||||||
|
this.stopContainer(containerId);
|
||||||
|
break;
|
||||||
|
case 'action-start':
|
||||||
|
this.startContainer(containerId);
|
||||||
|
break;
|
||||||
|
case 'action-restart':
|
||||||
|
this.restartContainer(containerId);
|
||||||
|
break;
|
||||||
|
case 'action-remove':
|
||||||
|
this.removeContainer(containerId);
|
||||||
|
break;
|
||||||
|
case 'action-unpause':
|
||||||
|
console.warn('Unpause action not implemented yet.');
|
||||||
|
break;
|
||||||
|
case 'action-update':
|
||||||
|
this.updateContainer(containerId, containerImage);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn('Unknown action:', action);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取容器状态对应的 CSS 类 - 保持
|
// 获取容器状态对应的 CSS 类 - 保持
|
||||||
@@ -326,13 +626,14 @@ const dockerManager = {
|
|||||||
return 'status-unknown';
|
return 'status-unknown';
|
||||||
},
|
},
|
||||||
|
|
||||||
// 设置下拉菜单动作的事件监听 (委托方法 - 现在直接使用按钮,不再需要)
|
// 设置下拉菜单动作的事件监听 - 简化为空方法,因为使用原生select不需要
|
||||||
setupActionDropdownListener() {
|
setupActionDropdownListener() {
|
||||||
// 这个方法留作兼容性,但实际上我们现在直接使用按钮而非下拉菜单
|
// 不需要特殊处理,使用原生select元素的change事件
|
||||||
},
|
},
|
||||||
|
|
||||||
// 查看日志 (示例:用 SweetAlert 显示)
|
// 查看日志
|
||||||
async showContainerLogs(containerId, containerName) {
|
async showContainerLogs(containerId, containerName) {
|
||||||
|
console.log('正在获取日志,容器ID:', containerId, '容器名称:', containerName);
|
||||||
core.showLoading('正在加载日志...');
|
core.showLoading('正在加载日志...');
|
||||||
try {
|
try {
|
||||||
// 注意: 后端 /api/docker/containers/:id/logs 需要存在并返回日志文本
|
// 注意: 后端 /api/docker/containers/:id/logs 需要存在并返回日志文本
|
||||||
@@ -357,7 +658,7 @@ const dockerManager = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.hideLoading();
|
core.hideLoading();
|
||||||
core.showAlert(`查看日志失败: ${error.message}`, 'error');
|
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');
|
core.showAlert(data.message || '容器停止成功', 'success');
|
||||||
systemStatus.refreshSystemStatus(); // 刷新整体状态
|
systemStatus.refreshSystemStatus(); // 刷新整体状态
|
||||||
|
|
||||||
|
// 刷新已停止容器列表
|
||||||
|
if (window.app && typeof window.app.refreshStoppedContainers === 'function') {
|
||||||
|
window.app.refreshStoppedContainers();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.hideLoading();
|
core.hideLoading();
|
||||||
core.showAlert(`停止容器失败: ${error.message}`, 'error');
|
core.showAlert(`停止容器失败: ${error.message}`, 'error');
|
||||||
@@ -491,6 +797,7 @@ const dockerManager = {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
confirmButtonColor: '#3085d6',
|
confirmButtonColor: '#3085d6',
|
||||||
cancelButtonColor: '#d33',
|
cancelButtonColor: '#d33',
|
||||||
|
width: '36em', // 增加弹窗宽度
|
||||||
inputValidator: (value) => {
|
inputValidator: (value) => {
|
||||||
if (!value || value.trim() === '') {
|
if (!value || value.trim() === '') {
|
||||||
return '镜像标签不能为空!';
|
return '镜像标签不能为空!';
|
||||||
@@ -511,6 +818,37 @@ const dockerManager = {
|
|||||||
confirmButton: 'update-confirm',
|
confirmButton: 'update-confirm',
|
||||||
cancelButton: 'update-cancel',
|
cancelButton: 'update-cancel',
|
||||||
footer: 'update-footer'
|
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.');
|
console.warn('[dockerManager] Troubleshoot button not found for binding.');
|
||||||
}
|
}
|
||||||
}, 0); // 延迟 0ms 执行,让浏览器有机会渲染
|
}, 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
|
timer: 3000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新已停止容器列表
|
||||||
|
if (window.app && typeof window.app.refreshStoppedContainers === 'function') {
|
||||||
|
window.app.refreshStoppedContainers();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// logger.error('刷新系统状态出错:', error);
|
// logger.error('刷新系统状态出错:', error);
|
||||||
showSystemStatusError(error.message);
|
showSystemStatusError(error.message);
|
||||||
|
|||||||
@@ -167,14 +167,18 @@ function isPasswordComplex(password) {
|
|||||||
function checkUcPasswordStrength() {
|
function checkUcPasswordStrength() {
|
||||||
const password = document.getElementById('ucNewPassword').value;
|
const password = document.getElementById('ucNewPassword').value;
|
||||||
const strengthSpan = document.getElementById('ucPasswordStrength');
|
const strengthSpan = document.getElementById('ucPasswordStrength');
|
||||||
|
const strengthBar = document.getElementById('strengthBar');
|
||||||
|
|
||||||
if (!password) {
|
if (!password) {
|
||||||
strengthSpan.textContent = '';
|
strengthSpan.textContent = '';
|
||||||
|
if (strengthBar) strengthBar.style.width = '0%';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let strength = 0;
|
let strength = 0;
|
||||||
let strengthText = '';
|
let strengthText = '';
|
||||||
|
let strengthColor = '';
|
||||||
|
let strengthWidth = '0%';
|
||||||
|
|
||||||
// 长度检查
|
// 长度检查
|
||||||
if (password.length >= 8) strength++;
|
if (password.length >= 8) strength++;
|
||||||
@@ -194,27 +198,93 @@ function checkUcPasswordStrength() {
|
|||||||
case 0:
|
case 0:
|
||||||
case 1:
|
case 1:
|
||||||
strengthText = '密码强度:非常弱';
|
strengthText = '密码强度:非常弱';
|
||||||
strengthSpan.style.color = '#FF4136';
|
strengthColor = '#FF4136';
|
||||||
|
strengthWidth = '20%';
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
strengthText = '密码强度:弱';
|
strengthText = '密码强度:弱';
|
||||||
strengthSpan.style.color = '#FF851B';
|
strengthColor = '#FF851B';
|
||||||
|
strengthWidth = '40%';
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
strengthText = '密码强度:中';
|
strengthText = '密码强度:中';
|
||||||
strengthSpan.style.color = '#FFDC00';
|
strengthColor = '#FFDC00';
|
||||||
|
strengthWidth = '60%';
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
strengthText = '密码强度:强';
|
strengthText = '密码强度:强';
|
||||||
strengthSpan.style.color = '#2ECC40';
|
strengthColor = '#2ECC40';
|
||||||
|
strengthWidth = '80%';
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
strengthText = '密码强度:非常强';
|
strengthText = '密码强度:非常强';
|
||||||
strengthSpan.style.color = '#3D9970';
|
strengthColor = '#3D9970';
|
||||||
|
strengthWidth = '100%';
|
||||||
break;
|
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,
|
checkUcPasswordStrength,
|
||||||
initUserCenter,
|
initUserCenter,
|
||||||
loadUserStats,
|
loadUserStats,
|
||||||
isPasswordComplex
|
isPasswordComplex,
|
||||||
|
togglePasswordVisibility,
|
||||||
|
refreshUserInfo
|
||||||
};
|
};
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
// 页面加载完成后初始化
|
||||||
|
|||||||
Reference in New Issue
Block a user