fix: Resolve frontend redirection issues when using images, and improve the backend management interface.

This commit is contained in:
dqzboy
2025-05-08 23:40:00 +08:00
parent aae122a352
commit 0d4bd10afe
17 changed files with 1346 additions and 929 deletions

1
.gitignore vendored
View File

@@ -159,3 +159,4 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
node_modules

View File

@@ -3,5 +3,5 @@
"content": "# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```",
"published": true,
"createdAt": "2025-04-01T21:36:40.369Z",
"updatedAt": "2025-04-01T21:36:41.977Z"
"updatedAt": "2025-05-08T15:16:47.900Z"
}

View File

@@ -30,6 +30,7 @@
"node-cache": "^5.1.2",
"p-limit": "^4.0.0",
"session-file-store": "^1.5.0",
"systeminformation": "^5.25.11",
"validator": "^13.7.0",
"ws": "^8.8.1"
},

View File

@@ -1,55 +1,118 @@
/**
* 系统服务模块 - 处理系统级信息获取
* 使用 systeminformation 库来提供跨平台的系统数据
*/
const { exec } = require('child_process');
const os = require('os');
const si = require('systeminformation');
const logger = require('../logger');
const os = require('os'); // os模块仍可用于某些特定情况或日志记录
// 获取磁盘空间信息
async function getDiskSpace() {
try {
// 根据操作系统不同有不同的命令
const isWindows = os.platform() === 'win32';
if (isWindows) {
// Windows实现(需要更复杂的逻辑)
return {
diskSpace: '未实现',
usagePercent: 0
};
} else {
// Linux/Mac实现
const diskInfo = await execPromise('df -h | grep -E "/$|/home" | head -1');
const diskParts = diskInfo.split(/\s+/);
if (diskParts.length >= 5) {
return {
diskSpace: `${diskParts[2]}/${diskParts[1]}`,
usagePercent: parseInt(diskParts[4].replace('%', ''))
};
} else {
throw new Error('磁盘信息格式不正确');
}
// Helper function to format bytes into a more readable format
function formatBytes(bytes, decimals = 2) {
if (bytes === null || bytes === undefined || isNaN(bytes) || bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
try {
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
} catch (e) {
return 'N/A'; // In case of Math.log error with very small numbers etc.
}
} catch (error) {
logger.error('获取磁盘空间失败:', error);
throw error;
}
}
// 辅助函数: 执行命令
function execPromise(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(error);
return;
}
resolve(stdout.trim());
});
});
// 获取核心系统资源信息 (CPU, Memory, Disk)
async function getSystemResources() {
try {
logger.info('Fetching system resources using systeminformation...');
// 并行获取数据以提高效率
const [cpuInfo, memInfo, fsInfo, cpuLoadInfo, osInfo] = await Promise.all([
si.cpu(), // For CPU model, cores, speed
si.mem(), // For memory details
si.fsSize(), // For filesystem details
si.currentLoad(), // For current CPU load percentage and per-core load
si.osInfo() // For OS type, mainly for specific disk selection if needed
]);
// --- CPU 信息处理 ---
let cpuUsage = parseFloat(cpuLoadInfo.currentLoad.toFixed(1));
// Fallback if currentLoad is not a number (very unlikely with systeminformation)
if (isNaN(cpuUsage) && Array.isArray(cpuLoadInfo.cpus) && cpuLoadInfo.cpus.length > 0) {
// Calculate average from per-core loads if overall isn't good
const totalLoad = cpuLoadInfo.cpus.reduce((acc, core) => acc + core.load, 0);
cpuUsage = parseFloat((totalLoad / cpuLoadInfo.cpus.length).toFixed(1));
}
if (isNaN(cpuUsage)) cpuUsage = null; // Final fallback to null if still NaN
const cpuData = {
cores: cpuInfo.cores,
physicalCores: cpuInfo.physicalCores,
model: cpuInfo.manufacturer + ' ' + cpuInfo.brand,
speed: cpuInfo.speed, // in GHz
usage: cpuUsage, // Overall CPU usage percentage
loadAvg: osInfo.platform !== 'win32' ? os.loadavg().map(load => parseFloat(load.toFixed(1))) : null // os.loadavg() is not for Windows
};
// --- 内存信息处理 ---
// systeminformation already provides these in bytes
const memData = {
total: memInfo.total,
free: memInfo.free, // Truly free
used: memInfo.used, // total - free (includes buff/cache on Linux, PhysMem used on macOS)
active: memInfo.active, // More representative of app-used memory
available: memInfo.available, // Memory available to applications (often free + reclaimable buff/cache)
wired: memInfo.wired, // macOS specific: memory that cannot be paged out
// compressed: memInfo.compressed, // macOS specific, if systeminformation lib provides it directly
buffcache: memInfo.buffcache // Linux specific: buffer and cache
};
// --- 磁盘信息处理 ---
// Find the primary disk (e.g., mounted on '/' for Linux/macOS, or C: for Windows)
let mainDiskInfo = null;
if (osInfo.platform === 'win32') {
mainDiskInfo = fsInfo.find(d => d.fs.startsWith('C:'));
} else {
mainDiskInfo = fsInfo.find(d => d.mount === '/');
}
if (!mainDiskInfo && fsInfo.length > 0) {
// Fallback to the first disk if the standard one isn't found
mainDiskInfo = fsInfo[0];
}
const diskData = mainDiskInfo ? {
mount: mainDiskInfo.mount,
size: formatBytes(mainDiskInfo.size),
used: formatBytes(mainDiskInfo.used),
available: formatBytes(mainDiskInfo.available), // systeminformation provides 'available'
percent: mainDiskInfo.use !== null && mainDiskInfo.use !== undefined ? mainDiskInfo.use.toFixed(0) + '%' : 'N/A'
} : {
mount: 'N/A',
size: 'N/A',
used: 'N/A',
available: 'N/A',
percent: 'N/A'
};
const resources = {
osType: osInfo.platform, // e.g., 'darwin', 'linux', 'win32'
osDistro: osInfo.distro,
cpu: cpuData,
memory: memData,
disk: diskData
};
logger.info('Successfully fetched system resources:', /* JSON.stringify(resources, null, 2) */ resources.osType);
return resources;
} catch (error) {
logger.error('获取系统资源失败 (services/systemService.js):', error);
// Return a structured error object or rethrow,
// so the API route can send an appropriate HTTP error
throw new Error(`Failed to get system resources: ${error.message}`);
}
}
module.exports = {
getDiskSpace
getSystemResources
// getDiskSpace, if it was previously exported and used by another route, can be kept
// or removed if getSystemResources now covers all disk info needs for /api/system-resources
};

View File

@@ -3,8 +3,8 @@
{
"username": "root",
"password": "$2b$10$lh1kqJtq3shL2BhMD1LbVOThGeAlPXsDgME/he4ZyDMRupVtj0Hl.",
"loginCount": 1,
"lastLogin": "2025-04-01T22:45:10.184Z"
"loginCount": 0,
"lastLogin": "2025-05-08T14:59:22.166Z"
}
]
}

View File

@@ -1518,6 +1518,272 @@
color: #333;
margin: 0;
}
/* 菜单管理 - 新增行样式 */
#new-menu-item-row input[type="text"],
#new-menu-item-row select {
/* 使用 Bootstrap 的 form-control-sm 已经减小了 padding 和 font-size */
margin-bottom: 0; /* 移除下方外边距,使其在表格行内更紧凑 */
}
#new-menu-item-row .form-control-sm {
padding: 0.3rem 0.6rem; /* 微调内边距 */
font-size: 0.875rem; /* 统一字体大小 */
}
#new-menu-item-row .form-select-sm {
padding: 0.3rem 1.5rem 0.3rem 0.6rem; /* 微调 select 内边距以适应箭头 */
font-size: 0.875rem;
}
.action-buttons-new-menu .btn {
margin-right: 5px; /* 按钮间距 */
min-width: 80px; /* 给按钮一个最小宽度 */
}
.action-buttons-new-menu .btn:last-child {
margin-right: 0;
}
.action-buttons-new-menu .btn i {
margin-right: 4px; /* 图标和文字间距 */
}
/* 使新行单元格垂直居中 */
#new-menu-item-row td {
vertical-align: middle;
}
/* 网络测试页面美化 */
#network-test {
/* 可以考虑将整个 #network-test 作为一个卡片,如果它还没有被 .content-section 样式化为卡片的话 */
}
/* 直接覆盖#testResults.loading的样式防止旋转 */
#network-test #testResults.loading {
animation: none !important;
border: none !important;
display: block !important;
position: relative !important;
text-align: center !important;
padding: 15px !important;
color: var(--text-secondary, #6c757d) !important;
font-size: 1rem !important;
width: auto !important;
height: auto !important;
}
/* 使用:before添加文本内容 */
#network-test #testResults.loading:before {
content: "测试进行中..." !important;
animation: none !important;
border: none !important;
position: absolute !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
color: var(--text-light, #e9ecef) !important;
font-size: 1rem !important;
font-weight: 500 !important;
padding: 0 !important;
background-color: transparent !important;
z-index: 10 !important;
text-align: center !important;
}
#network-test .form-row {
display: flex;
flex-wrap: wrap; /* 允许换行到小屏幕 */
gap: 1.5rem; /* 各个元素之间的间距 */
margin-bottom: 1.5rem;
align-items: flex-end; /* 使得按钮和选择框底部对齐 */
}
#network-test .form-group {
flex: 1; /* 让表单组占据可用空间 */
min-width: 250px; /* 避免在小屏幕上过于挤压 */
background-color: transparent; /* 移除之前 #network-test .input-group 的背景色 */
padding: 0; /* 移除内边距 */
box-shadow: none; /* 移除阴影 */
border-radius: 0;
}
#network-test label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-primary);
font-weight: 500;
font-size: 0.95rem;
}
#network-test .form-control,
#network-test .form-select {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background-color: var(--input-bg, var(--container-bg)); /* 允许自定义输入背景或使用容器背景 */
color: var(--text-primary);
font-size: 0.95rem;
transition: all var(--transition-fast);
box-shadow: var(--shadow-sm);
}
#network-test .form-control:focus,
#network-test .form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb, 61, 124, 244), 0.25); /* 使用RGB变量 */
outline: none;
}
#network-test .test-controls-container {
/* 这个容器包裹选择器和按钮 */
display: flex;
flex-wrap: wrap; /* 允许换行 */
gap: 1rem; /* 控件之间的间距 */
align-items: flex-end; /* 使得按钮和选择框底部对齐 */
margin-bottom: 1.5rem;
}
#network-test .test-controls-container .form-group {
flex-grow: 1;
flex-basis: 200px; /* 给每个控件一个基础宽度 */
}
#network-test .start-test-btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 500;
white-space: nowrap; /* 防止按钮文字换行 */
height: fit-content; /* 与调整后的 select 高度匹配 */
flex-shrink: 0; /* 防止按钮在 flex 布局中被压缩 */
}
#network-test .start-test-btn i {
margin-right: 0.5rem;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding: 0.5rem 0;
border-bottom: 1px solid var(--border-light);
}
.results-header h3 {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-light, #e9ecef); /* Changed to light color for dark background */
margin: 0;
}
#clearTestResultsBtn {
font-size: 0.85rem;
padding: 0.3rem 0.8rem;
}
#testResultsContainer {
background-color: var(--container-bg-dark, #1e2532); /* 使用变量或默认暗色 */
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
margin-top: 1rem;
padding: 0; /* 移除外层padding让头部和内容区自己控制 */
position: relative; /* Add this to be a positioning context for absolute children if needed by headers etc. */
}
#testResults {
padding: 1rem;
color: var(--text-light, #e9ecef);
font-family: var(--font-mono, 'JetBrains Mono', monospace);
font-size: 0.9rem;
white-space: pre-wrap;
word-break: break-all;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
background-color: transparent;
border-radius: 0 0 var(--radius-md) var(--radius-md);
box-shadow: none;
border: none;
margin-top: 0;
position: relative;
}
/* Use higher specificity and !important to force override */
#network-test #testResults.loading::before {
content: "测试进行中..."; /* Force text content */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* Center the text block */
color: var(--text-secondary, #6c757d);
font-size: 1rem;
font-weight: 500;
padding: 0.75rem 1.25rem;
background-color: transparent !important;
border: none !important; /* 移除边框 */
border-radius: var(--radius-md, 0.375rem);
z-index: 10;
text-align: center;
/* Force removal of spinner styles */
animation: none !important;
border-top-color: transparent !important; /* Force override any spinner head */
width: auto !important; /* Force width */
height: auto !important; /* Force height */
box-sizing: border-box; /* Ensure padding is included correctly */
}
/* Ensure keyframes for the spinner are removed or commented out */
/* @keyframes testResultSpinner { to { transform: rotate(360deg); } } */
/* Style for the <pre> tag inside #testResults for consistent output formatting */
#testResults pre {
margin: 0; /* Remove default pre margin */
font-family: inherit; /* Inherit from #testResults (monospace) */
font-size: inherit; /* Inherit from #testResults */
color: inherit; /* Inherit from #testResults */
white-space: pre-wrap; /* Ensure wrapping */
word-break: break-all; /* Ensure long lines break */
background-color: transparent; /* Ensure no pre background interferes */
padding: 0; /* No extra padding for pre, parent div handles it */
}
#testResults pre.text-danger {
color: var(--danger-color, #dc3545); /* Ensure error text is colored */
}
/* Placeholder text styling (if using <p class="text-muted...">) */
#testResults .text-muted.text-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: calc(100% - 2rem); /* Consider padding of #testResults */
}
/* 保持容器监控页面已停止容器列表的表头在鼠标悬停时背景色不变 */
#docker-monitoring .container-table th:hover {
background-color: var(--primary-color);
color: white;
}
/* SweetAlert2 弹窗内容文本居中 */
#swal2-html-container, /* 使用 ID 提高特异性 */
.swal2-html-container { /* 保留类选择器作为备用 */
text-align: center !important;
}
#swal2-html-container p,
.swal2-html-container p {
text-align: center !important;
}
#swal2-html-container div,
.swal2-html-container div {
text-align: center !important;
}
</style>
</head>
<body>
@@ -1636,11 +1902,6 @@
<!-- 文档管理部分 -->
<div id="documentation-management" class="content-section">
<h2 class="menu-label">文档管理</h2>
<div class="action-bar" style="margin-bottom: 15px;">
<button type="button" class="btn btn-primary" onclick="documentManager.newDocument()">
<i class="fas fa-plus"></i> 新建文档
</button>
</div>
<table id="documentTable">
<thead>
@@ -1650,6 +1911,12 @@
<!-- 文档列表将在这里动态添加 -->
</tbody>
</table>
<div class="action-bar" style="margin-top: 15px; margin-bottom: 15px;">
<button type="button" class="btn btn-primary add-btn" onclick="documentManager.newDocument()">
<i class="fas fa-plus"></i> 新建文档
</button>
</div>
<div id="editorContainer" style="display: none;">
<input type="text" id="documentTitle" placeholder="请输入文档标题" autocomplete="off">
@@ -1678,20 +1945,40 @@
<!-- 网络测试 -->
<div id="network-test" class="content-section">
<h1 class="admin-title">网络测试</h1>
<div class="input-group">
<label for="domainSelect">目标域名:</label>
<select id="domainSelect" class="form-control">
<!-- 选项将由 networkTest.initNetworkTest() 动态生成 -->
</select>
<div class="test-controls-container">
<div class="form-group">
<label for="domainSelect">目标域名:</label>
<select id="domainSelect" class="form-select">
<!-- 选项将由 networkTest.initNetworkTest() 动态生成 -->
</select>
</div>
<div class="form-group" id="customDomainContainer" style="display: none;">
<label for="customDomain">自定义域名:</label>
<input type="text" id="customDomain" class="form-control" placeholder="请输入域名,如 example.com">
</div>
<div class="form-group">
<label for="testType">测试类型:</label>
<select id="testType" class="form-select">
<!-- 选项将由 networkTest.initNetworkTest() 动态生成 -->
</select>
</div>
<button id="startTestBtn" class="btn btn-primary start-test-btn">
<i class="fas fa-play"></i> 开始测试
</button>
</div>
<div class="input-group">
<label for="testType">测试类型:</label>
<select id="testType">
<!-- 选项将由 networkTest.initNetworkTest() 动态生成 -->
</select>
<div id="testResultsContainer">
<div class="results-header">
<h3>测试结果</h3>
<button id="clearTestResultsBtn" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-times-circle"></i> 清空结果
</button>
</div>
<div id="testResults">
<!-- 测试结果将显示在这里 -->
</div>
</div>
<button class="btn btn-primary">开始测试</button>
<div id="testResults" style="margin-top: 20px; white-space: pre-wrap; font-family: monospace;"></div>
</div>
<!-- Docker服务状态 -->

View File

@@ -200,6 +200,7 @@
footer.innerHTML = `<p>Copyright © <span id="currentYear">${new Date().getFullYear()}</span> <span class="copyright-text">Docker-Proxy</span> All Rights Reserved. <a href="https://github.com/dqzboy/Docker-Proxy" target="_blank">GitHub</a></p>`;
}
}
window.protectCopyright = protectCopyright;
// ========================================
// === 文档加载相关函数 (移到此处) ===
@@ -208,7 +209,7 @@
async function loadAndDisplayDocumentation() {
// 防止重复加载
if (documentationLoaded) {
console.log('文档已加载,跳过重复加载');
// console.log('文档已加载,跳过重复加载');
return;
}
@@ -216,12 +217,12 @@
const docContentContainer = document.getElementById('documentationText');
if (!docListContainer || !docContentContainer) {
console.warn('找不到文档列表或内容容器,可能不是文档页面');
// console.warn('找不到文档列表或内容容器,可能不是文档页面');
return; // 如果容器不存在,则不执行加载
}
try {
console.log('开始加载文档列表和内容...');
// console.log('开始加载文档列表和内容...');
// 显示加载状态
docListContainer.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 正在加载文档列表...</div>';
@@ -234,7 +235,7 @@
}
const data = await response.json();
console.log('获取到文档列表:', data);
// console.log('获取到文档列表:', data);
// 保存到全局变量
window.documentationData = data;
@@ -322,9 +323,30 @@
}
}
}
// ========================================
// === 文档加载相关函数结束 ===
// ========================================
//
function useImage(imageName) {
// 切换到镜像加速标签页
switchTab('accelerate');
// 填充镜像名称到输入框
const imageInput = document.getElementById('imageInput');
if (imageInput) {
imageInput.value = imageName;
// 自动生成加速命令
generateCommands(imageName);
// 滚动到结果区域
const resultDiv = document.getElementById('result');
if (resultDiv) {
resultDiv.scrollIntoView({ behavior: 'smooth' });
}
}
// 显示用户友好的提示
showToastNotification(`已选择镜像: ${imageName}`, 'success');
}
window.useImage = useImage;
// ========================================
// === 全局变量和状态 ===
@@ -346,7 +368,7 @@
const config = await response.json();
if (config.proxyDomain) {
proxyDomain = config.proxyDomain;
console.log('成功加载代理域名:', proxyDomain);
// console.log('成功加载代理域名:', proxyDomain);
} else {
console.warn('配置中没有proxyDomain字段');
proxyDomain = 'registry-1.docker.io'; // 使用默认值
@@ -362,7 +384,7 @@
}
// ========================================
// === 新增:全局提示函数 ===
// === 全局提示函数 ===
// ========================================
function showToastNotification(message, type = 'info') { // types: info, success, error
// 移除任何现有的通知
@@ -438,19 +460,52 @@
loadAndDisplayDocumentation();
} else if (tabName === 'accelerate') {
// 重置显示状态
document.querySelector('.quick-guide').style.display = 'block';
document.querySelector('.popular-images').style.display = 'block';
document.getElementById('result').style.display = 'none';
const quickGuideEl = document.querySelector('.quick-guide');
if (quickGuideEl) quickGuideEl.style.display = 'block';
const popularImagesEl = document.querySelector('.popular-images');
if (popularImagesEl) popularImagesEl.style.display = 'block';
const accelerateFeaturesEl = document.querySelector('#accelerateContent .features');
if (accelerateFeaturesEl) accelerateFeaturesEl.style.display = 'grid';
const resultEl = document.getElementById('result');
if (resultEl) resultEl.style.display = 'none';
const imageInput = document.getElementById('imageInput').value.trim();
if (imageInput) {
generateCommands(imageInput);
}
document.getElementById('searchInput').value = '';
document.getElementById('searchResults').innerHTML = '';
document.querySelector('.popular-images').style.display = 'block';
// 清空搜索相关的输入和结果,因为我们切换到了加速标签
const searchInputEl = document.getElementById('searchInput');
if(searchInputEl) searchInputEl.value = '';
const searchResultsEl = document.getElementById('searchResults');
if(searchResultsEl) searchResultsEl.innerHTML = '';
}
}
window.switchTab = switchTab;
// 新增:返回搜索结果视图
function showSearchResults() {
const searchResultsList = document.getElementById('searchResultsList');
const imageTagsView = document.getElementById('imageTagsView');
const searchResults = document.getElementById('searchResults');
const paginationContainer = document.getElementById('paginationContainer');
const features = document.querySelector('#searchContent .features'); // 获取特性区域
if (searchResultsList) searchResultsList.style.display = 'block';
if (imageTagsView) imageTagsView.style.display = 'none';
// 检查 searchResults 是否有内容并且不是 "未找到" 消息
if (searchResults && searchResults.innerHTML.trim() !== '' && !searchResults.querySelector('.empty-result')) {
searchResults.style.display = 'block';
if (paginationContainer) paginationContainer.style.display = 'flex';
if (features) features.style.display = 'none'; // 隐藏特性区
} else {
// 如果 searchResults 为空, 或者包含 "未找到" 消息
if (searchResults) searchResults.style.display = 'block'; // 保持 searchResults 区域可见以显示 "未找到"
if (paginationContainer) paginationContainer.style.display = 'none';
if (features) features.style.display = 'grid'; // 显示特性区
}
}
window.showSearchResults = showSearchResults;
// 添加formatNumber函数定义
function formatNumber(num) {
@@ -467,15 +522,18 @@
}
// 生成加速命令
function generateCommands(imageInput) {
if (!imageInput) {
imageInput = document.getElementById('imageInput').value.trim();
function generateCommands(imageNameInput) {
let currentImageName = imageNameInput;
if (!currentImageName) {
const imageInputEl = document.getElementById('imageInput');
if (imageInputEl) currentImageName = imageInputEl.value.trim();
}
if (!imageInput) {
if (!currentImageName) {
alert('请输入 Docker 镜像名称');
return;
}
let [imageName, tag] = imageInput.split(':');
let [imageName, tag] = currentImageName.split(':');
tag = tag || 'latest';
let originalImage = `${imageName}:${tag}`;
@@ -518,48 +576,46 @@
});
// 显示结果并隐藏其他内容
resultDiv.style.display = 'flex';
resultDiv.style.flexDirection = 'column';
document.querySelector('.quick-guide').style.display = 'none';
document.querySelector('.features').style.display = 'none';
if (resultDiv) {
resultDiv.style.display = 'flex';
resultDiv.style.flexDirection = 'column';
}
const quickGuideEl = document.querySelector('.quick-guide');
if (quickGuideEl) quickGuideEl.style.display = 'none';
const accelerateFeaturesEl = document.querySelector('#accelerateContent .features');
if (accelerateFeaturesEl) accelerateFeaturesEl.style.display = 'none';
}
window.generateCommands = generateCommands;
// 复制命令到剪贴板
function copyToClipboard(text, element) {
// console.log('[copyToClipboard] Received text to copy:', text); // Debug log
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
showNotification();
showToastNotification('已复制到剪贴板', 'success');
}, (err) => {
console.error('无法复制文本: ', err);
showToastNotification('复制失败: ' + err.message, 'error');
});
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showNotification();
try {
document.execCommand('copy');
showToastNotification('已复制到剪贴板', 'success');
} catch (err) {
console.error('无法使用 execCommand 复制文本: ', err);
showToastNotification('复制失败: ' + err.message, 'error');
} finally {
document.body.removeChild(textarea);
}
}
}
function showNotification() {
// 移除任何现有的通知
const existingNotification = document.querySelector('.copy-success');
if (existingNotification) {
existingNotification.remove();
}
// 创建新的通知
const successDiv = document.createElement('div');
successDiv.className = 'copy-success';
successDiv.textContent = '已复制到剪贴板';
// 添加到body而不是pre元素
document.body.appendChild(successDiv);
// 1.5秒后移除提示
setTimeout(() => {
successDiv.remove();
}, 1500);
}
window.copyToClipboard = copyToClipboard;
// 改进的API请求函数支持自动重试
async function fetchWithRetry(url, options = {}, retries = 3, retryDelay = 1000) {
@@ -619,7 +675,7 @@
document.getElementById('imageTagsView').style.display = 'none';
try {
console.log(`搜索Docker Hub: 关键词=${searchTerm}, 页码=${page}`);
// console.log(`搜索Docker Hub: 关键词=${searchTerm}, 页码=${page}`);
// 使用新的fetchWithRetry函数
const data = await fetchWithRetry(
@@ -662,6 +718,7 @@
document.getElementById('paginationContainer').style.display = 'none';
}
}
window.searchDockerHub = searchDockerHub;
// 更新分页控件
function updatePagination(currentPage, totalPages) {
@@ -748,10 +805,10 @@
try {
// 使用新的fetchWithRetry函数获取标签计数
const countApiUrl = `/api/dockerhub/tag-count?name=${encodeURIComponent(currentImageData.name)}&official=${currentImageData.isOfficial}`;
console.log('Requesting tag count from:', countApiUrl);
// console.log('Requesting tag count from:', countApiUrl);
const countData = await fetchWithRetry(countApiUrl);
console.log('Received tag count data:', countData);
// console.log('Received tag count data:', countData);
const tagCount = countData.count || 0;
const recommendedMode = countData.recommended_mode || 'paginated';
@@ -837,6 +894,7 @@
showToastNotification(`加载镜像详情失败: ${error.message}`, 'error');
}
}
window.viewImageDetails = viewImageDetails;
// 新增: 加载所有标签 - 改进错误处理
async function loadAllTags() {
@@ -965,6 +1023,7 @@
loadAllTagsBtn.innerHTML = '<i class="fas fa-cloud-download-alt"></i> 加载全部TAG';
}
}
window.loadAllTags = loadAllTags;
// 添加 loadImageTags 函数定义
async function loadImageTags(page = 1) {
@@ -978,11 +1037,11 @@
try {
// 构建API URL
const apiUrl = `/api/dockerhub/tags?name=${encodeURIComponent(currentImageData.name)}&official=${currentImageData.isOfficial}&page=${page}&page_size=25`;
console.log('Requesting tags from:', apiUrl);
// console.log('Requesting tags from:', apiUrl);
// 使用fetchWithRetry获取数据
const data = await fetchWithRetry(apiUrl);
console.log('Received tags data:', data);
// console.log('Received tags data:', data);
currentTagPage = page; // 更新当前页码
if (data.results && data.results.length > 0) {
@@ -1028,6 +1087,7 @@
showToastNotification(`加载标签失败: ${error.message}`, 'error');
}
}
window.loadImageTags = loadImageTags;
// 新增: 显示客户端分页控制器
function displayClientPagination(totalPages) {
@@ -1190,7 +1250,7 @@
tableContainer.appendChild(tagTable); // 将表格添加到容器中
// 添加调试信息
console.log(`显示了 ${tags.length} 个标签`);
// console.log(`显示了 ${tags.length} 个标签`);
}
// 新增的排序标签函数
@@ -1375,7 +1435,7 @@
const tagsResults = document.getElementById('tagsResults');
// 保留搜索统计信息
const statsHTML = tagsResults.innerHTML.split('</div>')[0] + '</div>';
tagsResults.innerHTML = statsHTML + '<div class="no-filter-results"><p>没有匹配 "' + searchTerm + '" 的TAG</p></div>';
tagsResults.innerHTML = statsHTML + '<div class="no-filter-results"><p>没有匹配 "' + searchTerm + '" 的标签</p></div>';
}
return; // 已处理全局搜索,不继续执行
@@ -1421,6 +1481,7 @@
noResultsEl.remove();
}
}
window.filterTags = filterTags;
// 添加重置搜索功能
function resetTagSearch() {
@@ -1442,6 +1503,7 @@
loadImageTags(currentTagPage);
}
}
window.resetTagSearch = resetTagSearch;
// 修改标签搜索容器,添加重置按钮
function enhanceTagSearchContainer() {
@@ -1467,7 +1529,7 @@
// 显示指定的文档
function showDocument(index) {
console.log('显示文档索引:', index);
// console.log('显示文档索引:', index);
if (!window.documentationData || !Array.isArray(window.documentationData)) {
console.error('文档数据不可用');
@@ -1497,7 +1559,7 @@
return;
}
console.log('文档数据:', doc);
// console.log('文档数据:', doc);
// 高亮选中的文档
const docLinks = document.querySelectorAll('.doc-list li a');
@@ -1521,18 +1583,18 @@
// 如果文档内容不存在,则需要获取完整内容
if (!doc.content) {
const docId = doc.id || doc._id;
console.log('获取文档内容ID:', docId);
// console.log('获取文档内容ID:', docId);
fetch(`/api/documentation/${docId}`)
.then(response => {
console.log('文档API响应:', response.status, response.statusText);
// console.log('文档API响应:', response.status, response.statusText);
if (!response.ok) {
throw new Error(`获取文档内容失败: ${response.status}`);
}
return response.json();
})
.then(fullDoc => {
console.log('获取到完整文档:', fullDoc);
// console.log('获取到完整文档:', fullDoc);
// 更新缓存的文档内容
window.documentationData[docIndex].content = fullDoc.content;
@@ -1555,15 +1617,13 @@
renderDocumentContent(docContent, doc);
}
}
// 确保showDocument函数在全局范围内可用
window.showDocument = showDocument;
// 渲染文档内容
function renderDocumentContent(container, doc) {
if (!container) return;
console.log('正在渲染文档:', doc);
// console.log('正在渲染文档:', doc);
// 确保有内容可渲染
if (!doc.content && !doc.path) {
@@ -1584,14 +1644,14 @@
// 如果是文件路径但无内容,尝试获取
fetch(`/api/documentation/file?path=${encodeURIComponent(doc.id + '.md')}`)
.then(response => {
console.log('文件内容响应:', response.status, response.statusText);
// console.log('文件内容响应:', response.status, response.statusText);
if (!response.ok) {
throw new Error(`获取文件内容失败: ${response.status}`);
}
return response.text();
})
.then(content => {
console.log('获取到文件内容,长度:', content.length);
// console.log('获取到文件内容,长度:', content.length);
doc.content = content;
renderMarkdownContent(container, doc);
})
@@ -1612,20 +1672,60 @@
function renderMarkdownContent(container, doc) {
if (!container) return;
console.log('渲染Markdown内容:', doc.title, '内容长度:', doc.content ? doc.content.length : 0);
// console.log('渲染Markdown内容:', doc.title, '内容长度:', doc.content ? doc.content.length : 0);
if (doc.content) {
// 使用marked渲染Markdown内容
if (window.marked) {
try {
const parsedContent = marked.parse(doc.content);
// 结构:内容(含标题)-> 元数据
container.innerHTML = `
<div class="doc-content">${parsedContent}</div>
<div class="doc-meta">
${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
</div>
`;
const rawHtml = marked.parse(doc.content);
// 创建一个临时的根元素来容纳和处理已解析的Markdown内容
const docFragmentRoot = document.createElement('div');
docFragmentRoot.innerHTML = rawHtml;
// 在这个临时根元素中查找所有的 <pre> 元素
const preElements = docFragmentRoot.querySelectorAll('pre');
preElements.forEach(preElement => {
const codeElement = preElement.querySelector('code');
let codeToCopy = '';
if (codeElement) {
codeToCopy = codeElement.textContent;
} else {
codeToCopy = preElement.textContent;
}
if (codeToCopy.trim() !== '') {
const copyButton = document.createElement('button');
copyButton.className = 'copy-btn'; // 应用现有样式
copyButton.textContent = '复制';
copyButton.onclick = function() { // 事件监听器在此处附加到按钮对象
// console.log('[Tutorial Copy Button] Attempting to copy:', codeToCopy); // 保留此调试日志
copyToClipboard(codeToCopy, this);
};
preElement.style.position = 'relative';
preElement.appendChild(copyButton); // 按钮被追加到 docFragmentRoot 内的 preElement
}
});
// 清空页面上的主容器
container.innerHTML = '';
// 创建 .doc-content div 并将处理过的文档片段追加进去
const docContentDiv = document.createElement('div');
docContentDiv.className = 'doc-content';
// 将 docFragmentRoot 的所有子节点移动到 docContentDiv以避免多余的包裹 div
while (docFragmentRoot.firstChild) {
docContentDiv.appendChild(docFragmentRoot.firstChild);
}
container.appendChild(docContentDiv); // docContentDiv 现在包含带有活动按钮的 PRE 元素
// 创建并追加 .doc-meta div
const docMetaDiv = document.createElement('div');
docMetaDiv.className = 'doc-meta';
docMetaDiv.innerHTML = `${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}`;
container.appendChild(docMetaDiv);
} catch (error) {
console.error('Markdown解析失败:', error);
// 发生错误时仍然显示原始Markdown内容 + Meta

View File

@@ -1,94 +1,94 @@
// 应用程序入口模块
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM 加载完成,初始化模块...');
// console.log('DOM 加载完成,初始化模块...');
// 启动应用程序
core.initApp();
// 在核心应用初始化后,再初始化其他模块
initializeModules();
console.log('模块初始化已启动');
// console.log('模块初始化已启动');
});
// 初始化所有模块
async function initializeModules() {
console.log('开始初始化所有模块...');
// console.log('开始初始化所有模块...');
try {
// 初始化核心模块
console.log('正在初始化核心模块...');
// console.log('正在初始化核心模块...');
if (typeof core !== 'undefined') {
// core.init() 已经在core.initApp()中调用,这里不再重复调用
console.log('核心模块初始化完成');
// console.log('核心模块初始化完成');
} else {
console.error('核心模块未定义');
// console.error('核心模块未定义');
}
// 初始化认证模块
console.log('正在初始化认证模块...');
// console.log('正在初始化认证模块...');
if (typeof auth !== 'undefined') {
await auth.init();
console.log('认证模块初始化完成');
// console.log('认证模块初始化完成');
} else {
console.error('认证模块未定义');
// console.error('认证模块未定义');
}
// 初始化用户中心
console.log('正在初始化用户中心...');
// console.log('正在初始化用户中心...');
if (typeof userCenter !== 'undefined') {
await userCenter.init();
console.log('用户中心初始化完成');
// console.log('用户中心初始化完成');
} else {
console.error('用户中心未定义');
// console.error('用户中心未定义');
}
// 初始化菜单管理
console.log('正在初始化菜单管理...');
// console.log('正在初始化菜单管理...');
if (typeof menuManager !== 'undefined') {
await menuManager.init();
console.log('菜单管理初始化完成');
// console.log('菜单管理初始化完成');
} else {
console.error('菜单管理未定义');
// console.error('菜单管理未定义');
}
// 初始化文档管理
console.log('正在初始化文档管理...');
// console.log('正在初始化文档管理...');
if (typeof documentManager !== 'undefined') {
await documentManager.init();
console.log('文档管理初始化完成');
// console.log('文档管理初始化完成');
} else {
console.error('文档管理未定义');
// console.error('文档管理未定义');
}
// 初始化Docker管理
console.log('正在初始化Docker管理...');
// console.log('正在初始化Docker管理...');
if (typeof dockerManager !== 'undefined') {
await dockerManager.init();
console.log('Docker管理初始化完成');
// console.log('Docker管理初始化完成');
} else {
console.error('Docker管理未定义');
// console.error('Docker管理未定义');
}
// 初始化系统状态
console.log('正在初始化系统状态...');
// console.log('正在初始化系统状态...');
if (typeof systemStatus !== 'undefined') {
if (typeof systemStatus.initDashboard === 'function') {
await systemStatus.initDashboard();
console.log('系统状态初始化完成');
// console.log('系统状态初始化完成');
} else {
console.error('systemStatus.initDashboard 函数未定义!');
// console.error('systemStatus.initDashboard 函数未定义!');
}
} else {
console.error('系统状态未定义');
// console.error('系统状态未定义');
}
// 初始化网络测试
console.log('正在初始化网络测试...');
// console.log('正在初始化网络测试...');
if (typeof networkTest !== 'undefined') {
await networkTest.init();
console.log('网络测试初始化完成');
// console.log('网络测试初始化完成');
} else {
console.error('网络测试未定义');
// console.error('网络测试未定义');
}
// 加载监控配置
@@ -97,31 +97,31 @@ async function initializeModules() {
// 显示默认页面 - 使用core中的showSection函数
core.showSection('dashboard');
console.log('所有模块初始化完成');
// console.log('所有模块初始化完成');
} catch (error) {
console.error('初始化模块时发生错误:', error);
// console.error('初始化模块时发生错误:', error);
// 尝试使用 core.showAlert如果 core 本身加载失败则用 console.error
if (typeof core !== 'undefined' && core.showAlert) {
core.showAlert('初始化失败: ' + error.message, 'error');
} else {
console.error('核心模块无法加载,无法显示警告弹窗');
// console.error('核心模块无法加载,无法显示警告弹窗');
}
}
}
// 监控配置相关函数
function loadMonitoringConfig() {
console.log('正在加载监控配置...');
// console.log('正在加载监控配置...');
fetch('/api/monitoring-config')
.then(response => {
console.log('监控配置API响应:', response.status, response.statusText);
// console.log('监控配置API响应:', response.status, response.statusText);
if (!response.ok) {
throw new Error(`HTTP状态错误 ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(config => {
console.log('获取到监控配置:', config);
// console.log('获取到监控配置:', config);
// 填充表单
document.getElementById('notificationType').value = config.notificationType || 'wechat';
document.getElementById('webhookUrl').value = config.webhookUrl || '';
@@ -141,10 +141,10 @@ function loadMonitoringConfig() {
document.getElementById('toggleMonitoringBtn').textContent =
config.isEnabled ? '禁用监控' : '启用监控';
console.log('监控配置加载完成');
// console.log('监控配置加载完成');
})
.catch(error => {
console.error('加载监控配置失败:', error);
// console.error('加载监控配置失败:', error);
// 使用安全的方式调用core.showAlert
if (typeof core !== 'undefined' && core && typeof core.showAlert === 'function') {
core.showAlert('加载监控配置失败: ' + error.message, 'error');
@@ -230,7 +230,7 @@ function testNotification() {
});
})
.catch(error => {
console.error('测试通知失败:', error);
// console.error('测试通知失败:', error);
Swal.fire({
icon: 'error',
title: '发送失败',

View File

@@ -46,7 +46,7 @@ async function login() {
// 注销函数
async function logout() {
console.log("注销操作被触发");
// console.log("注销操作被触发");
try {
core.showLoading();
const response = await fetch('/api/logout', { method: 'POST' });
@@ -62,7 +62,7 @@ async function logout() {
throw new Error('退出登录失败');
}
} catch (error) {
console.error('退出登录失败:', error);
// console.error('退出登录失败:', error);
core.showAlert('退出登录失败: ' + error.message, 'error');
// 即使API失败也清除本地状态
localStorage.removeItem('isLoggedIn');
@@ -84,7 +84,7 @@ async function refreshCaptcha() {
const data = await response.json();
document.getElementById('captchaText').textContent = data.captcha;
} catch (error) {
console.error('刷新验证码失败:', error);
// console.error('刷新验证码失败:', error);
document.getElementById('captchaText').textContent = '验证码加载失败,点击重试';
}
}
@@ -110,7 +110,7 @@ function showLoginModal() {
// 导出模块
const auth = {
init: function() {
console.log('初始化认证模块...');
// console.log('初始化认证模块...');
// 在这里可以添加认证模块初始化的相关代码
return Promise.resolve(); // 返回一个已解决的 Promise保持与其他模块一致
},

View File

@@ -13,21 +13,21 @@ let systemConfig = {};
* 检查登录状态,加载基础配置
*/
async function initApp() {
console.log('初始化应用...');
console.log('-------------调试信息开始-------------');
console.log('当前URL:', window.location.href);
console.log('浏览器信息:', navigator.userAgent);
console.log('DOM已加载状态:', document.readyState);
// console.log('初始化应用...');
// console.log('-------------调试信息开始-------------');
// console.log('当前URL:', window.location.href);
// console.log('浏览器信息:', navigator.userAgent);
// console.log('DOM已加载状态:', document.readyState);
// 检查当前页面是否为登录页
const isLoginPage = window.location.pathname.includes('admin');
console.log('是否为管理页面:', isLoginPage);
// console.log('是否为管理页面:', isLoginPage);
try {
// 检查会话状态
const sessionResult = await checkSession();
const isAuthenticated = sessionResult.authenticated;
console.log('会话检查结果:', isAuthenticated);
// console.log('会话检查结果:', isAuthenticated);
// 检查localStorage中的登录状态 (主要用于刷新页面时保持UI)
const localLoginState = localStorage.getItem('isLoggedIn') === 'true';
@@ -40,12 +40,12 @@ async function initApp() {
if (isLoginPage) {
// 在登录页,但会话有效,显示管理界面
console.log('已登录,显示管理界面...');
// console.log('已登录,显示管理界面...');
await loadSystemConfig();
showAdminInterface();
} else {
// 在非登录页,正常显示
console.log('已登录,继续应用初始化...');
// console.log('已登录,继续应用初始化...');
await loadSystemConfig();
showAdminInterface(); // 确保管理界面可见
}
@@ -56,27 +56,27 @@ async function initApp() {
if (!isLoginPage) {
// 在非登录页,重定向到登录页
console.log('未登录,重定向到登录页...');
// console.log('未登录,重定向到登录页...');
window.location.href = '/admin';
return false;
} else {
// 在登录页,显示登录框
console.log('未登录,显示登录模态框...');
// console.log('未登录,显示登录模态框...');
hideLoadingIndicator();
showLoginModal();
}
}
console.log('应用初始化完成');
console.log('-------------调试信息结束-------------');
// console.log('应用初始化完成');
// console.log('-------------调试信息结束-------------');
return isAuthenticated;
} catch (error) {
console.error('初始化应用失败:', error);
console.log('-------------调试错误信息-------------');
console.log('错误堆栈:', error.stack);
console.log('错误类型:', error.name);
console.log('错误消息:', error.message);
console.log('---------------------------------------');
// console.error('初始化应用失败:', error);
// console.log('-------------调试错误信息-------------');
// console.log('错误堆栈:', error.stack);
// console.log('错误类型:', error.name);
// console.log('错误消息:', error.message);
// console.log('---------------------------------------');
showAlert('加载应用失败:' + error.message, 'error');
hideLoadingIndicator();
showLoginModal();
@@ -111,7 +111,7 @@ async function checkSession() {
authenticated: false
};
} catch (error) {
console.error('检查会话状态出错:', error);
// console.error('检查会话状态出错:', error);
return {
authenticated: false,
error: error.message
@@ -133,12 +133,12 @@ function loadSystemConfig() {
return response.json();
})
.then(config => {
console.log('加载配置成功:', config);
// console.log('加载配置成功:', config);
// 应用配置
applySystemConfig(config);
})
.catch(error => {
console.error('加载配置失败:', error);
// console.error('加载配置失败:', error);
showAlert('加载配置失败: ' + error.message, 'warning');
});
}
@@ -157,18 +157,18 @@ function applySystemConfig(config) {
* 显示管理界面
*/
function showAdminInterface() {
console.log('开始显示管理界面...');
// console.log('开始显示管理界面...');
hideLoadingIndicator();
const adminContainer = document.getElementById('adminContainer');
if (adminContainer) {
console.log('找到管理界面容器,设置为显示');
// console.log('找到管理界面容器,设置为显示');
adminContainer.style.display = 'flex';
} else {
console.error('未找到管理界面容器元素 #adminContainer');
// console.error('未找到管理界面容器元素 #adminContainer');
}
console.log('管理界面已显示,正在初始化事件监听器');
// console.log('管理界面已显示,正在初始化事件监听器');
// 初始化菜单事件监听
initEventListeners();
@@ -178,13 +178,13 @@ function showAdminInterface() {
* 隐藏加载提示器
*/
function hideLoadingIndicator() {
console.log('正在隐藏加载提示器...');
// console.log('正在隐藏加载提示器...');
const loadingIndicator = document.getElementById('loadingIndicator');
if (loadingIndicator) {
loadingIndicator.style.display = 'none';
console.log('加载提示器已隐藏');
// console.log('加载提示器已隐藏');
} else {
console.warn('未找到加载提示器元素 #loadingIndicator');
// console.warn('未找到加载提示器元素 #loadingIndicator');
}
}
@@ -331,54 +331,47 @@ function throttle(func, wait = 300) {
* 初始化事件监听
*/
function initEventListeners() {
console.log('开始初始化事件监听器...');
// console.log('开始初始化事件监听器...');
// 侧边栏菜单切换事件
// 侧边栏菜单事件
const menuItems = document.querySelectorAll('.sidebar-nav li');
console.log('找到侧边栏菜单项数量:', menuItems.length);
if (menuItems.length > 0) {
menuItems.forEach((item, index) => {
const sectionId = item.getAttribute('data-section');
console.log(`绑定事件到菜单项 #${index+1}: ${sectionId}`);
item.addEventListener('click', function() {
const sectionId = this.getAttribute('data-section');
showSection(sectionId);
});
});
console.log('侧边栏菜单事件监听器已绑定');
} else {
console.error('未找到侧边栏菜单项 .sidebar-nav li');
}
// console.log('找到侧边栏菜单项数量:', menuItems.length);
menuItems.forEach((item, index) => {
const section = item.getAttribute('data-section');
// console.log(`绑定事件到菜单项 #${index + 1}: ${section}`);
item.addEventListener('click', () => showSection(section));
});
// console.log('侧边栏菜单事件监听器已绑定');
// 用户中心按钮
const userCenterBtn = document.getElementById('userCenterBtn');
if (userCenterBtn) {
console.log('找到用户中心按钮,绑定事件');
// console.log('找到用户中心按钮,绑定事件');
userCenterBtn.addEventListener('click', () => showSection('user-center'));
} else {
console.warn('未找到用户中心按钮 #userCenterBtn');
}
// 登出按钮
// 登出按钮 (侧边栏)
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
console.log('找到登出按钮,绑定事件');
logoutBtn.addEventListener('click', () => auth.logout());
} else {
console.warn('未找到登出按钮 #logoutBtn');
// console.log('找到登出按钮,绑定事件');
logoutBtn.addEventListener('click', () => {
if (window.auth && typeof window.auth.logout === 'function') {
window.auth.logout();
}
});
}
// 用户中心内登出按钮
// 登出按钮 (用户中心)
const ucLogoutBtn = document.getElementById('ucLogoutBtn');
if (ucLogoutBtn) {
console.log('找到用户中心内登出按钮,绑定事件');
ucLogoutBtn.addEventListener('click', () => auth.logout());
} else {
console.warn('未找到用户中心内登出按钮 #ucLogoutBtn');
// console.log('找到用户中心内登出按钮,绑定事件');
ucLogoutBtn.addEventListener('click', () => {
if (window.auth && typeof window.auth.logout === 'function') {
window.auth.logout();
}
});
}
console.log('事件监听器初始化完成');
// console.log('事件监听器初始化完成');
}
/**
@@ -386,77 +379,108 @@ function initEventListeners() {
* @param {string} sectionId 要显示的内容区域ID
*/
function showSection(sectionId) {
console.log(`尝试显示内容区域: ${sectionId}`);
// 获取所有内容区域和菜单项
// console.log('尝试显示内容区域:', sectionId);
const contentSections = document.querySelectorAll('.content-section');
const menuItems = document.querySelectorAll('.sidebar-nav li');
console.log(`找到 ${contentSections.length} 个内容区域和 ${menuItems.length} 个菜单项`);
// console.log('找到', contentSections.length, '个内容区域和', menuItems.length, '个菜单项');
let sectionFound = false;
let menuItemFound = false;
// 隐藏所有内容区域,取消激活所有菜单项
contentSections.forEach(section => {
section.classList.remove('active');
if (section.id === sectionId) {
section.classList.add('active');
sectionFound = true;
// console.log('成功激活内容区域:', sectionId);
} else {
section.classList.remove('active');
}
});
if (!sectionFound) {
// console.warn('未找到要显示的内容区域:', sectionId, '. 默认显示dashboard.');
document.getElementById('dashboard').classList.add('active');
sectionId = 'dashboard'; // 更新 sectionId 以便正确高亮菜单
}
let menuItemFound = false;
menuItems.forEach(item => {
item.classList.remove('active');
if (item.getAttribute('data-section') === sectionId) {
item.classList.add('active');
menuItemFound = true;
// console.log('成功激活菜单项:', sectionId);
} else {
item.classList.remove('active');
}
});
// 激活指定的内容区域
const targetSection = document.getElementById(sectionId);
if (targetSection) {
targetSection.classList.add('active');
sectionFound = true;
console.log(`成功激活内容区域: ${sectionId}`);
// 特殊处理:切换到用户中心时,确保用户信息已加载
if (sectionId === 'user-center' && window.userCenter) {
console.log('切换到用户中心,调用 getUserInfo()');
window.userCenter.getUserInfo();
}
} else {
console.error(`未找到指定的内容区域: ${sectionId}`);
if (!menuItemFound) {
// console.warn('未找到要激活的菜单项 for section:', sectionId);
// 尝试激活 dashboard 作为回退
const dashboardItem = document.querySelector('.sidebar-nav li[data-section="dashboard"]');
if(dashboardItem) dashboardItem.classList.add('active');
}
// 激活相应的菜单项
const targetMenuItem = document.querySelector(`.sidebar-nav li[data-section="${sectionId}"]`);
if (targetMenuItem) {
targetMenuItem.classList.add('active');
menuItemFound = true;
console.log(`成功激活菜单项: ${sectionId}`);
} else {
console.error(`未找到对应的菜单项: ${sectionId}`);
}
// 如果没有找到指定的内容区域和菜单项,显示仪表盘
if (!sectionFound && !menuItemFound) {
console.warn(`未找到指定的内容区域和菜单项,将显示默认仪表盘`);
const dashboard = document.getElementById('dashboard');
if (dashboard) {
dashboard.classList.add('active');
const dashboardMenuItem = document.querySelector('.sidebar-nav li[data-section="dashboard"]');
if (dashboardMenuItem) {
dashboardMenuItem.classList.add('active');
}
}
}
// 切换内容区域后可能需要执行的额外操作
// 更新 URL hash
window.location.hash = sectionId;
// 刷新特定部分的内容,例如仪表盘
if (sectionId === 'dashboard') {
console.log('已激活仪表盘,无需再次刷新系统状态');
// 不再自动刷新系统状态,仅在首次加载或用户手动点击刷新按钮时刷新
// console.log('已激活仪表盘,无需再次刷新系统状态');
// if (window.systemStatus && typeof window.systemStatus.refreshSystemStatus === 'function') {
// window.systemStatus.refreshSystemStatus();
// }
}
// console.log('内容区域切换完成:', sectionId);
}
/**
* 根据URL hash显示对应的区域
*/
function showSectionFromHash() {
const hash = window.location.hash.substring(1); // 移除 '#' 号
if (hash) {
// console.log('从URL Hash加载区域:', hash);
showSection(hash);
} else {
// console.log('URL Hash为空默认显示dashboard区域');
showSection('dashboard'); // 默认显示仪表盘
}
}
/**
* 切换加载状态
* @param {boolean} isLoading - 是否正在加载
* @param {string} elementId - 目标元素ID
* @param {string} originalText - 原始文本(可选)
*/
function toggleLoadingState(isLoading, elementId, originalText = null) {
const element = document.getElementById(elementId);
if (!element) {
// console.warn(`未找到元素 ${elementId} 来切换加载状态`);
return;
}
if (isLoading) {
element.disabled = true;
// 可以考虑保存原始文本,如果按钮文本被修改了
if (originalText && !element.dataset.originalText) {
element.dataset.originalText = element.innerHTML;
}
element.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 加载中...';
} else {
element.disabled = false;
if (element.dataset.originalText) {
element.innerHTML = element.dataset.originalText;
} else if (originalText) { // 如果没有保存的原始文本,但传入了,也使用
element.innerHTML = originalText;
}
// 如果按钮文本没有被修改为 "加载中...",则不需要恢复
}
console.log(`内容区域切换完成: ${sectionId}`);
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM已加载正在初始化应用...');
// console.log('DOM已加载正在初始化应用...');
initApp();
// 检查URL参数处理消息提示等
@@ -475,25 +499,22 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// 导出核心对象
const core = {
isLoggedIn,
// 导出核心函数和变量 (如果需要在其他模块直接使用)
window.core = {
initApp,
checkSession,
loadSystemConfig,
applySystemConfig,
showLoading,
hideLoading,
showAdminInterface,
hideLoadingIndicator,
showLoginModal,
showAlert,
showConfirm,
showLoading,
hideLoading,
showSection,
showSectionFromHash,
formatDateTime,
debounce,
throttle,
initEventListeners,
showSection
toggleLoadingState
};
// 全局公开核心模块
window.core = core;

View File

@@ -13,12 +13,12 @@ let editorMd = null;
const documentManager = {
// 初始化文档管理
init: function() {
console.log('初始化文档管理模块...');
// console.log('初始化文档管理模块...');
// 渲染表头
this.renderDocumentTableHeader();
// 加载文档列表
return this.loadDocuments().catch(err => {
console.error('加载文档列表失败:', err);
// console.error('加载文档列表失败:', err);
return Promise.resolve(); // 即使失败也继续初始化过程
});
},
@@ -28,7 +28,7 @@ const documentManager = {
try {
const documentTable = document.getElementById('documentTable');
if (!documentTable) {
console.warn('文档管理表格元素 (id=\"documentTable\") 未找到,无法渲染表头。');
// console.warn('文档管理表格元素 (id=\"documentTable\") 未找到,无法渲染表头。');
return;
}
@@ -37,7 +37,7 @@ const documentManager = {
if (!thead) {
thead = document.createElement('thead');
documentTable.insertBefore(thead, documentTable.firstChild); // 确保 thead 在 tbody 之前
console.log('创建了文档表格的 thead 元素。');
// console.log('创建了文档表格的 thead 元素。');
}
// 设置表头内容 (包含 ID 列)
@@ -51,9 +51,9 @@ const documentManager = {
<th style="width: 20%">操作</th>
</tr>
`;
console.log('文档表格表头已渲染。');
// console.log('文档表格表头已渲染。');
} catch (error) {
console.error('渲染文档表格表头时出错:', error);
// console.error('渲染文档表格表头时出错:', error);
}
},
@@ -78,11 +78,11 @@ const documentManager = {
});
if (sessionResponse.status === 401) {
console.warn('会话已过期,无法加载文档');
// console.warn('会话已过期,无法加载文档');
sessionValid = false;
}
} catch (sessionError) {
console.warn('检查会话状态发生网络错误:', sessionError);
// console.warn('检查会话状态发生网络错误:', sessionError);
// 发生网络错误时继续尝试加载文档
}
@@ -98,7 +98,7 @@ const documentManager = {
for (const path of possiblePaths) {
try {
console.log(`尝试从 ${path} 获取文档列表`);
// console.log(`尝试从 ${path} 获取文档列表`);
const response = await fetch(path, {
credentials: 'same-origin',
headers: {
@@ -108,7 +108,7 @@ const documentManager = {
});
if (response.status === 401) {
console.warn(`API路径 ${path} 返回未授权状态`);
// console.warn(`API路径 ${path} 返回未授权状态`);
authError = true;
continue;
}
@@ -138,18 +138,18 @@ const documentManager = {
// 先渲染表头,再渲染文档列表
this.renderDocumentTableHeader();
this.renderDocumentList();
console.log(`成功从API路径 ${path} 加载文档列表`, documents);
// console.log(`成功从API路径 ${path} 加载文档列表`, documents);
success = true;
break;
}
} catch (e) {
console.warn(`${path} 加载文档失败:`, e);
// console.warn(`从 ${path} 加载文档失败:`, e);
}
}
// 处理认证错误 - 只有当会话检查和API请求都明确失败时才强制登出
if ((authError || !sessionValid) && !success && localStorage.getItem('isLoggedIn') === 'true') {
console.warn('会话检查和API请求均指示会话已过期');
// console.warn('会话检查和API请求均指示会话已过期');
core.showAlert('会话已过期,请重新登录', 'warning');
setTimeout(() => {
localStorage.removeItem('isLoggedIn');
@@ -160,14 +160,14 @@ const documentManager = {
// 如果API请求失败但不是认证错误显示空文档列表
if (!success) {
console.log('API请求失败显示空文档列表');
// console.log('API请求失败显示空文档列表');
documents = [];
// 仍然需要渲染表头
this.renderDocumentTableHeader();
this.renderDocumentList();
}
} catch (error) {
console.error('加载文档失败:', error);
// console.error('加载文档失败:', error);
// 在UI上显示错误
const documentTableBody = document.getElementById('documentTableBody');
if (documentTableBody) {
@@ -184,16 +184,16 @@ const documentManager = {
try {
const editorContainer = document.getElementById('editor');
if (!editorContainer) {
console.error('找不到编辑器容器元素');
// console.error('找不到编辑器容器元素');
return;
}
// 检查 toastui 是否已加载
console.log('检查编辑器依赖项:', typeof toastui);
// console.log('检查编辑器依赖项:', typeof toastui);
// 确保 toastui 对象存在
if (typeof toastui === 'undefined') {
console.error('Toast UI Editor 未加载');
// console.error('Toast UI Editor 未加载');
return;
}
@@ -213,9 +213,9 @@ const documentManager = {
]
});
console.log('编辑器初始化完成', editorMd);
// console.log('编辑器初始化完成', editorMd);
} catch (error) {
console.error('初始化编辑器出错:', error);
// console.error('初始化编辑器出错:', error);
core.showAlert('初始化编辑器失败: ' + error.message, 'error');
}
},
@@ -292,11 +292,11 @@ const documentManager = {
});
if (sessionResponse.status === 401) {
console.warn('会话已过期,无法保存文档');
// console.warn('会话已过期,无法保存文档');
sessionValid = false;
}
} catch (sessionError) {
console.warn('检查会话状态发生网络错误:', sessionError);
// console.warn('检查会话状态发生网络错误:', sessionError);
// 发生网络错误时继续尝试保存操作
}
@@ -326,7 +326,7 @@ const documentManager = {
const method = currentDocument && currentDocument.id ? 'PUT' : 'POST';
console.log(`尝试${method === 'PUT' ? '更新' : '创建'}文档,标题: ${title}`);
// console.log(`尝试${method === 'PUT' ? '更新' : '创建'}文档,标题: ${title}`);
const response = await fetch(apiUrl, {
method: method,
@@ -345,7 +345,7 @@ const documentManager = {
// 处理响应
if (response.status === 401) {
// 明确的未授权响应
console.warn('保存文档返回401未授权');
// console.warn('保存文档返回401未授权');
core.showAlert('未登录或会话已过期,请重新登录', 'warning');
setTimeout(() => {
localStorage.removeItem('isLoggedIn');
@@ -367,7 +367,7 @@ const documentManager = {
}
const savedDoc = await response.json();
console.log('保存的文档:', savedDoc);
// console.log('保存的文档:', savedDoc);
// 确保savedDoc包含必要的时间字段
if (savedDoc) {
@@ -387,11 +387,11 @@ const documentManager = {
createdAt: fullDoc.createdAt,
updatedAt: fullDoc.updatedAt
});
console.log('获取到完整的文档时间信息:', fullDoc);
// console.log('获取到完整的文档时间信息:', fullDoc);
}
}
} catch (timeError) {
console.warn('获取文档完整时间信息失败:', timeError);
// console.warn('获取文档完整时间信息失败:', timeError);
}
}
@@ -408,7 +408,7 @@ const documentManager = {
this.hideEditor();
await this.loadDocuments(); // 重新加载文档列表
} catch (error) {
console.error('保存文档失败:', error);
// console.error('保存文档失败:', error);
core.showAlert('保存文档失败: ' + error.message, 'error');
} finally {
core.hideLoading();
@@ -470,7 +470,7 @@ const documentManager = {
updatedAt = '未更新';
}
} catch (error) {
console.warn(`解析文档时间失败:`, error, doc);
// console.warn(`解析文档时间失败:`, error, doc);
}
const statusClasses = doc.published ? 'status-badge status-running' : 'status-badge status-stopped';
@@ -505,10 +505,10 @@ const documentManager = {
// 编辑文档
editDocument: async function(id) {
try {
console.log(`准备编辑文档ID: ${id}`);
core.showLoading();
// console.log(`准备编辑文档ID: ${id}`);
core.showLoading('加载文档中...'); // 更明确的加载提示
// 检查会话状态,优化会话检查逻辑
// 会话检查逻辑保持不变
let sessionValid = true;
try {
const sessionResponse = await fetch('/api/check-session', {
@@ -518,183 +518,67 @@ const documentManager = {
},
credentials: 'same-origin'
});
if (sessionResponse.status === 401) {
console.warn('会话已过期,无法编辑文档');
sessionValid = false;
}
} catch (sessionError) {
console.warn('检查会话状态发生网络错误:', sessionError);
// 发生网络错误时不立即判定会话失效,继续尝试编辑操作
if (sessionResponse.status === 401) sessionValid = false;
} catch (sessionError) {
// console.warn('检查会话状态发生网络错误:', sessionError);
// 继续尝试API调用时会再次处理401
}
// 只有在明确会话无效时才提示重新登录
if (!sessionValid) {
core.showAlert('您的会话已过期,请重新登录', 'warning');
setTimeout(() => {
localStorage.removeItem('isLoggedIn');
window.location.reload();
}, 1500);
auth.showLoginModal(); // 使用 auth 模块显示登录
core.hideLoading();
return;
}
// 在本地查找文档
currentDocument = documents.find(doc => doc.id === id);
// 不再依赖本地缓存的列表项获取content始终从API获取完整文档
// console.log('始终从API获取完整文档详情进行编辑ID:', id);
// 如果本地未找到从API获取
if (!currentDocument && id) {
try {
console.log('从API获取文档详情');
// 尝试多个可能的API路径
const apiPaths = [
`/api/documents/${id}`,
`/api/documentation/${id}`
];
let docResponse = null;
let authError = false;
for (const apiPath of apiPaths) {
try {
console.log(`尝试从 ${apiPath} 获取文档`);
const response = await fetch(apiPath, {
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
// 只有明确401错误才认定为会话过期
if (response.status === 401) {
console.warn(`API ${apiPath} 返回401未授权`);
authError = true;
continue;
}
if (response.ok) {
docResponse = response;
console.log(`成功从 ${apiPath} 获取文档`);
break;
}
} catch (pathError) {
console.warn(`${apiPath} 获取文档失败:`, pathError);
}
}
// 处理认证错误
if (authError && !docResponse) {
core.showAlert('未登录或会话已过期,请重新登录', 'warning');
setTimeout(() => {
localStorage.removeItem('isLoggedIn');
window.location.reload();
}, 1500);
return;
}
if (docResponse && docResponse.ok) {
currentDocument = await docResponse.json();
console.log('获取到文档详情:', currentDocument);
// 确保文档包含必要的时间字段
if (!currentDocument.createdAt && currentDocument.updatedAt) {
// 如果没有创建时间但有更新时间,使用更新时间
currentDocument.createdAt = currentDocument.updatedAt;
} else if (!currentDocument.createdAt) {
// 如果都没有,使用当前时间
currentDocument.createdAt = new Date().toISOString();
}
if (!currentDocument.updatedAt) {
// 如果没有更新时间,使用创建时间
currentDocument.updatedAt = currentDocument.createdAt;
}
// 将获取到的文档添加到文档列表中
const existingIndex = documents.findIndex(d => d.id === id);
if (existingIndex >= 0) {
documents[existingIndex] = currentDocument;
} else {
documents.push(currentDocument);
}
} else {
throw new Error('所有API路径都无法获取文档');
}
} catch (apiError) {
console.error('从API获取文档详情失败:', apiError);
core.showAlert('获取文档详情失败: ' + apiError.message, 'error');
}
}
// 如果仍然没有找到文档,显示错误
if (!currentDocument) {
core.showAlert('未找到指定的文档', 'error');
const response = await fetch(`/api/documents/${id}`, {
headers: {
'Cache-Control': 'no-cache', // 确保获取最新数据
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
});
if (response.status === 401) {
core.showAlert('您的会话已过期或无权限访问此文档,请重新登录', 'warning');
auth.showLoginModal();
core.hideLoading();
return;
}
// 显示编辑器界面并设置内容
this.showEditor();
if (!response.ok) {
const errorText = await response.text();
// console.error(`获取文档失败 (${response.status}):`, errorText);
throw new Error(`获取文档内容失败: ${errorText || response.status}`);
}
const docToEdit = await response.json();
currentDocument = docToEdit; // 更新当前编辑的文档对象
// 确保编辑器已初始化
if (!editorMd) {
await new Promise(resolve => setTimeout(resolve, 100));
this.initEditor();
await new Promise(resolve => setTimeout(resolve, 500));
// 等待编辑器初始化完成后再继续
// 使用短延时确保编辑器DOM完全准备好
await new Promise(resolve => setTimeout(resolve, 100));
}
// 设置文档内容
if (editorMd) {
document.getElementById('documentTitle').value = currentDocument.title || '';
if (currentDocument.content) {
console.log(`设置文档内容,长度: ${currentDocument.content.length}`);
editorMd.setMarkdown(currentDocument.content);
} else {
console.log('文档内容为空,尝试额外获取内容');
// 如果文档内容为空,尝试额外获取内容
try {
const contentResponse = await fetch(`/api/documents/${id}/content`, {
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
// 只有明确401错误才提示重新登录
if (contentResponse.status === 401) {
core.showAlert('会话已过期,请重新登录', 'warning');
setTimeout(() => {
localStorage.removeItem('isLoggedIn');
window.location.reload();
}, 1500);
return;
}
if (contentResponse.ok) {
const contentData = await contentResponse.json();
if (contentData.content) {
currentDocument.content = contentData.content;
editorMd.setMarkdown(contentData.content);
console.log('成功获取额外内容');
}
}
} catch (contentError) {
console.warn('获取额外内容失败:', contentError);
}
// 如果仍然没有内容,设置为空
if (!currentDocument.content) {
editorMd.setMarkdown('');
}
}
} else {
console.error('编辑器初始化失败,无法设置内容');
core.showAlert('编辑器初始化失败,请刷新页面重试', 'error');
if (!editorMd) {
core.showAlert('编辑器初始化失败,无法编辑文档。', 'error');
core.hideLoading();
return;
}
document.getElementById('documentTitle').value = docToEdit.title || '';
editorMd.setMarkdown(docToEdit.content || '');
this.showEditor();
} catch (error) {
console.error('编辑文档时出错:', error);
core.showAlert('编辑文档失败: ' + error.message, 'error');
// console.error('编辑文档时出错:', error);
core.showAlert(`加载文档进行编辑失败: ${error.message}`, 'error');
} finally {
core.hideLoading();
}
@@ -736,7 +620,7 @@ const documentManager = {
}).then(async (result) => {
if (result.isConfirmed) {
try {
console.log(`尝试删除文档: ${id}`);
// console.log(`尝试删除文档: ${id}`);
// 检查会话状态
const sessionResponse = await fetch('/api/check-session', {
@@ -778,11 +662,11 @@ const documentManager = {
throw new Error(errorData.error || '删除文档失败');
}
console.log('文档删除成功响应:', await response.json());
// console.log('文档删除成功响应:', await response.json());
core.showAlert('文档已成功删除', 'success');
await this.loadDocuments(); // 重新加载文档列表
} catch (error) {
console.error('删除文档失败:', error);
// console.error('删除文档失败:', error);
core.showAlert('删除文档失败: ' + error.message, 'error');
}
}
@@ -815,7 +699,7 @@ const documentManager = {
return;
}
console.log(`尝试切换文档 ${id} 的发布状态,当前状态:`, doc.published);
// console.log(`尝试切换文档 ${id} 的发布状态,当前状态:`, doc.published);
// 构建更新请求数据
const updateData = {
@@ -852,7 +736,7 @@ const documentManager = {
// 更新成功
const updatedDoc = await response.json();
console.log('文档状态更新响应:', updatedDoc);
// console.log('文档状态更新响应:', updatedDoc);
// 更新本地文档列表
const docIndex = documents.findIndex(d => d.id === id);
@@ -863,7 +747,7 @@ const documentManager = {
core.showAlert('文档状态已更新', 'success');
} catch (error) {
console.error('更改发布状态失败:', error);
// console.error('更改发布状态失败:', error);
core.showAlert('更改发布状态失败: ' + error.message, 'error');
} finally {
core.hideLoading();
@@ -880,7 +764,7 @@ window.documentManager = documentManager;
*/
async function showDocument(docId) {
try {
console.log('正在获取文档内容ID:', docId);
// console.log('正在获取文档内容ID:', docId);
// 显示加载状态
const documentContent = document.getElementById('documentContent');
@@ -895,7 +779,7 @@ async function showDocument(docId) {
}
const doc = await response.json();
console.log('获取到文档:', doc);
// console.log('获取到文档:', doc);
// 更新文档内容区域
if (documentContent) {
@@ -916,13 +800,13 @@ async function showDocument(docId) {
`;
}
} else {
console.error('找不到文档内容容器ID: documentContent');
// console.error('找不到文档内容容器ID: documentContent');
}
// 高亮当前选中的文档
highlightSelectedDocument(docId);
} catch (error) {
console.error('获取文档内容失败:', error);
// console.error('获取文档内容失败:', error);
// 显示错误信息
const documentContent = document.getElementById('documentContent');

View File

@@ -10,7 +10,7 @@ let currentConfig = {}; // 保存当前完整的配置
const menuManager = {
// 初始化菜单管理
init: async function() {
console.log('初始化菜单管理 (config.json)...');
// console.log('初始化菜单管理 (config.json)...');
this.renderMenuTableHeader(); // 渲染表头
await this.loadMenuItems(); // 加载菜单项
return Promise.resolve();
@@ -54,10 +54,10 @@ const menuManager = {
configMenuItems = currentConfig.menuItems || []; // 提取菜单项,如果不存在则为空数组
this.renderMenuItems();
console.log('成功从 /api/config 加载菜单项', configMenuItems);
// console.log('成功从 /api/config 加载菜单项', configMenuItems);
} catch (error) {
console.error('加载菜单项失败:', error);
// console.error('加载菜单项失败:', error);
const menuTableBody = document.getElementById('menuTableBody');
if (menuTableBody) {
menuTableBody.innerHTML = `
@@ -83,7 +83,7 @@ const menuManager = {
menuTableBody.innerHTML = ''; // 清空现有内容
if (!Array.isArray(configMenuItems)) {
console.error("configMenuItems 不是一个数组:", configMenuItems);
// console.error("configMenuItems 不是一个数组:", configMenuItems);
menuTableBody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: #ff4d4f;">菜单数据格式错误</td></tr>';
return;
}
@@ -114,26 +114,37 @@ const menuManager = {
const menuTableBody = document.getElementById('menuTableBody');
if (!menuTableBody) return;
// 如果已存在新行,则不重复添加
if (document.getElementById('new-menu-item-row')) {
document.getElementById('new-text').focus();
return;
}
const newRow = document.createElement('tr');
newRow.id = 'new-menu-item-row';
newRow.className = 'new-item-row';
newRow.className = 'new-item-row'; // 可以为此行添加特定样式
newRow.innerHTML = `
<td>#</td>
<td><input type="text" id="new-text" placeholder="菜单文本"></td>
<td><input type="text" id="new-link" placeholder="链接地址"></td>
<td><input type="text" id="new-text" class="form-control form-control-sm" placeholder="菜单文本"></td>
<td><input type="text" id="new-link" class="form-control form-control-sm" placeholder="链接地址 (例如 /about 或 https://example.com)"></td>
<td>
<select id="new-newTab">
<select id="new-newTab" class="form-select form-select-sm">
<option value="false">否</option>
<option value="true">是</option>
</select>
</td>
<td>
<button class="action-btn" onclick="menuManager.saveNewMenuItem()">保存</button>
<button class="action-btn" onclick="menuManager.cancelNewMenuItem()">取消</button>
<td class="action-buttons-new-menu">
<button class="btn btn-sm btn-success save-new-menu-btn" onclick="menuManager.saveNewMenuItem()">
<i class="fas fa-save"></i> 保存
</button>
<button class="btn btn-sm btn-danger cancel-new-menu-btn" onclick="menuManager.cancelNewMenuItem()">
<i class="fas fa-times"></i> 取消
</button>
</td>
`;
menuTableBody.appendChild(newRow);
// 将新行添加到表格体的最上方
menuTableBody.insertBefore(newRow, menuTableBody.firstChild);
document.getElementById('new-text').focus();
},
@@ -174,7 +185,7 @@ const menuManager = {
core.showAlert('菜单项已添加', 'success');
} catch (error) {
console.error('添加菜单项失败:', error);
// console.error('添加菜单项失败:', error);
core.showAlert('添加菜单项失败: ' + error.message, 'error');
}
},
@@ -505,7 +516,7 @@ const menuManager = {
});
} catch (error) {
console.error('更新菜单项失败:', error);
// console.error('更新菜单项失败:', error);
Swal.fire({
icon: 'error',
title: '保存失败',
@@ -562,7 +573,7 @@ const menuManager = {
core.showAlert('菜单项已删除', 'success');
} catch (error) {
console.error('删除菜单项失败:', error);
// console.error('删除菜单项失败:', error);
core.showAlert('删除菜单项失败: ' + error.message, 'error');
}
});

View File

@@ -8,14 +8,14 @@ let menuLoaded = false;
// 立即执行初始化函数
(function() {
console.log('[菜单模块] 初始化开始');
// console.log('[菜单模块] 初始化开始');
try {
// 页面加载完成后执行,但不在这里调用加载函数
document.addEventListener('DOMContentLoaded', function() {
console.log('[菜单模块] DOM内容加载完成等待loadMenu或loadNavMenu调用');
// console.log('[菜单模块] DOM内容加载完成等待loadMenu或loadNavMenu调用');
// 不在这里调用,避免重复加载
});
console.log('[菜单模块] 初始化完成,等待调用');
// console.log('[菜单模块] 初始化完成,等待调用');
} catch (error) {
console.error('[菜单模块] 初始化失败:', error);
}
@@ -24,11 +24,11 @@ let menuLoaded = false;
// 加载导航菜单
async function loadNavMenu() {
if (menuLoaded) {
console.log('[菜单模块] 菜单已加载,跳过');
// console.log('[菜单模块] 菜单已加载,跳过');
return;
}
console.log('[菜单模块] loadNavMenu() 函数被调用');
// console.log('[菜单模块] loadNavMenu() 函数被调用');
const navMenu = document.getElementById('navMenu');
if (!navMenu) {
console.error('[菜单模块] 无法找到id为navMenu的元素菜单加载失败');
@@ -36,18 +36,18 @@ async function loadNavMenu() {
}
try {
console.log('[菜单模块] 正在从/api/config获取菜单配置...');
// console.log('[菜单模块] 正在从/api/config获取菜单配置...');
// 从API获取配置
const response = await fetch('/api/config');
console.log('[菜单模块] API响应状态:', response.status, response.statusText);
// console.log('[菜单模块] API响应状态:', response.status, response.statusText);
if (!response.ok) {
throw new Error(`获取配置失败: ${response.status} ${response.statusText}`);
}
const config = await response.json();
console.log('[菜单模块] 成功获取配置:', config);
// console.log('[菜单模块] 成功获取配置:', config);
// 确保menuItems存在且是数组
if (!config.menuItems || !Array.isArray(config.menuItems) || config.menuItems.length === 0) {
@@ -70,7 +70,7 @@ async function loadNavMenu() {
// 渲染导航菜单
function renderNavMenu(navMenuElement, menuItems) {
try {
console.log('[菜单模块] 开始渲染导航菜单,菜单项数量:', menuItems.length);
// console.log('[菜单模块] 开始渲染导航菜单,菜单项数量:', menuItems.length);
// 清空现有内容
navMenuElement.innerHTML = '';
@@ -94,7 +94,7 @@ function renderNavMenu(navMenuElement, menuItems) {
// 添加菜单项
menuItems.forEach((item, index) => {
console.log(`[菜单模块] 渲染菜单项 #${index+1}:`, item);
// console.log(`[菜单模块] 渲染菜单项 #${index+1}:`, item);
const menuItem = document.createElement('li');
menuItem.style.marginLeft = '25px'; // 增加间距
@@ -136,11 +136,11 @@ function renderNavMenu(navMenuElement, menuItems) {
// 绑定移动端菜单切换事件
menuToggle.addEventListener('click', () => {
console.log('[菜单模块] 菜单切换按钮被点击');
// console.log('[菜单模块] 菜单切换按钮被点击');
navMenuElement.classList.toggle('active');
});
console.log(`[菜单模块] 成功渲染了 ${menuItems.length} 个导航菜单项`);
// console.log(`[菜单模块] 成功渲染了 ${menuItems.length} 个导航菜单项`);
} catch (error) {
console.error('[菜单模块] 渲染导航菜单失败:', error);
navMenuElement.innerHTML = `<span class="menu-error" style="color: #ff6b6b; font-size: 14px;">菜单渲染失败: ${error.message}</span>`;
@@ -149,6 +149,6 @@ function renderNavMenu(navMenuElement, menuItems) {
// 添加loadMenu函数作为loadNavMenu的别名确保与index.html中的调用匹配
function loadMenu() {
console.log('[菜单模块] 调用loadMenu() - 转发到loadNavMenu()');
// console.log('[菜单模块] 调用loadMenu() - 转发到loadNavMenu()');
loadNavMenu();
}

View File

@@ -4,16 +4,27 @@
const networkTest = {
// 初始化函数
init: function() {
console.log('初始化网络测试模块...');
this.initNetworkTest();
return Promise.resolve();
// console.log('初始化网络测试模块...');
this.initNetworkTestControls(); // Renamed for clarity
this.displayInitialResultsMessage();
return Promise.resolve(); // Keep if other inits expect a Promise
},
// 初始化网络测试界面
initNetworkTest: function() {
displayInitialResultsMessage: function() {
const resultsDiv = document.getElementById('testResults');
if (resultsDiv) {
resultsDiv.innerHTML = '<p class="text-muted text-center p-3">请选择参数并开始测试。</p>';
}
},
// 初始化网络测试界面控件和事件
initNetworkTestControls: function() {
const domainSelect = document.getElementById('domainSelect');
const testType = document.getElementById('testType');
const testTypeSelect = document.getElementById('testType'); // Corrected ID reference
const startTestButton = document.getElementById('startTestBtn'); // Use ID
const clearResultsButton = document.getElementById('clearTestResultsBtn');
const resultsDiv = document.getElementById('testResults');
// 填充域名选择器
if (domainSelect) {
domainSelect.innerHTML = `
@@ -26,68 +37,128 @@ const networkTest = {
<option value="mcr.microsoft.com">mcr.microsoft.com</option>
<option value="docker.elastic.co">docker.elastic.co</option>
<option value="registry-1.docker.io">registry-1.docker.io</option>
<option value="google.com">google.com (测试用)</option>
<option value="cloudflare.com">cloudflare.com (测试用)</option>
<option value="custom">自定义域名</option>
`;
// 添加选择变化事件,显示/隐藏自定义域名输入框
domainSelect.addEventListener('change', () => {
const customDomainContainer = document.getElementById('customDomainContainer');
if (customDomainContainer) {
customDomainContainer.style.display = domainSelect.value === 'custom' ? 'block' : 'none';
}
});
}
// 填充测试类型选择器
if (testType) {
testType.innerHTML = `
<option value="ping">Ping</option>
if (testTypeSelect) {
testTypeSelect.innerHTML = `
<option value="ping">Ping (ICMP)</option>
<option value="traceroute">Traceroute</option>
`;
}
// 绑定测试按钮点击事件
const testButton = document.querySelector('#network-test button');
if (testButton) {
testButton.addEventListener('click', this.runNetworkTest);
// 绑定开始测试按钮点击事件
if (startTestButton) {
// Bind the function correctly, preserving 'this' context if necessary,
// or ensure runNetworkTest doesn't rely on 'this' from the event handler.
// Using an arrow function or .bind(this) if runNetworkTest uses 'this.someOtherMethod'
startTestButton.addEventListener('click', () => this.runNetworkTest());
} else {
// console.error('未找到开始测试按钮 (ID: startTestBtn)');
}
// 绑定清空结果按钮点击事件
if (clearResultsButton && resultsDiv) {
clearResultsButton.addEventListener('click', () => {
resultsDiv.innerHTML = '<p class="text-muted text-center p-3">结果已清空。</p>';
// Optionally, remove loading class if it was somehow stuck
resultsDiv.classList.remove('loading');
});
} else {
if (!clearResultsButton) { /* console.error('未找到清空结果按钮 (ID: clearTestResultsBtn)'); */ }
if (!resultsDiv) { /* console.error('未找到测试结果区域 (ID: testResults)'); */ }
}
},
// 运行网络测试
runNetworkTest: function() {
const domain = document.getElementById('domainSelect').value;
runNetworkTest: async function() { // Changed to async for await
let domain = document.getElementById('domainSelect').value;
const testType = document.getElementById('testType').value;
const resultsDiv = document.getElementById('testResults');
const startTestButton = document.getElementById('startTestBtn');
// 验证选择了域名
if (!domain) {
core.showAlert('请选择目标域名', 'error');
// 处理自定义域名
if (domain === 'custom') {
const customDomain = document.getElementById('customDomain')?.value?.trim();
if (!customDomain) {
core.showAlert('请输入自定义域名进行测试。', 'warning');
return;
}
domain = customDomain;
} else if (!domain) {
core.showAlert('请选择目标域名进行测试。', 'warning');
return;
}
resultsDiv.innerHTML = '测试中,请稍候...';
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60秒超时
if (!testType) {
core.showAlert('请选择测试类型。', 'warning');
return;
}
fetch('/api/network-test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ domain, type: testType }),
signal: controller.signal
})
.then(response => {
resultsDiv.innerHTML = ''; // Clear previous content before adding loading class
resultsDiv.classList.add('loading');
if(startTestButton) startTestButton.disabled = true; // Disable button during test
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
// logger.warn('Network test aborted due to timeout.');
}, 60000); // 60秒超时
try {
const response = await fetch('/api/network-test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Add any necessary auth headers if your API requires them
// 'Authorization': 'Bearer ' + localStorage.getItem('authToken'),
},
body: JSON.stringify({ domain, type: testType }),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error('网络测试失败');
const errorText = await response.text();
let detail = errorText;
try {
const errorJson = JSON.parse(errorText);
detail = errorJson.message || errorJson.error || errorText;
} catch (e) { /* ignore parsing error, use raw text */ }
throw new Error(`网络连接正常,但测试执行失败 (状态: ${response.status}): ${detail}`);
}
return response.text();
})
.then(result => {
resultsDiv.textContent = result;
})
.catch(error => {
console.error('网络测试出错:', error);
const result = await response.text();
// Format the plain text result in a <pre> tag for better display
resultsDiv.innerHTML = `<pre>${result}</pre>`;
} catch (error) {
clearTimeout(timeoutId); // Ensure timeout is cleared on any error
// console.error('网络测试出错:', error);
let errorMessage = '测试失败: ' + error.message;
if (error.name === 'AbortError') {
resultsDiv.textContent = '测试超时,请稍后再试';
} else {
resultsDiv.textContent = '测试失败: ' + error.message;
errorMessage = '测试请求超时 (60秒)。请检查网络连接或目标主机状态。';
}
});
resultsDiv.innerHTML = `<pre class="text-danger">${errorMessage}</pre>`;
} finally {
resultsDiv.classList.remove('loading');
if(startTestButton) startTestButton.disabled = false; // Re-enable button
}
}
};
// 全局公开网络测试模块
// 全局公开网络测试模块 (或者 integrate with app.js module system if you have one)
// Ensure this is called after the DOM is ready, e.g., in app.js or a DOMContentLoaded listener
// For now, let's assume app.js handles calling networkTest.init()
window.networkTest = networkTest;

View File

@@ -10,41 +10,41 @@ let DEBUG_MODE = false; // 控制是否输出调试信息
const logger = {
debug: function(...args) {
if (DEBUG_MODE) {
console.log('[systemStatus:DEBUG]', ...args);
// console.log('[systemStatus:DEBUG]', ...args);
}
},
log: function(...args) {
console.log('[systemStatus]', ...args);
// console.log('[systemStatus]', ...args);
},
warn: function(...args) {
console.warn('[systemStatus]', ...args);
// console.warn('[systemStatus]', ...args);
},
error: function(...args) {
console.error('[systemStatus]', ...args);
// console.error('[systemStatus]', ...args);
},
// 开启或关闭调试模式
setDebug: function(enabled) {
DEBUG_MODE = !!enabled;
console.log(`[systemStatus] 调试模式已${DEBUG_MODE ? '开启' : '关闭'}`);
// console.log(`[systemStatus] 调试模式已${DEBUG_MODE ? '开启' : '关闭'}`);
}
};
// 刷新系统状态
async function refreshSystemStatus() {
logger.log('刷新系统状态...');
// logger.log('刷新系统状态...');
showSystemStatusLoading(); // 显示表格/活动列表的加载状态
showDashboardLoading(); // 显示仪表盘卡片的加载状态
try {
// 并行获取Docker状态和系统资源信息
logger.log('获取Docker状态和系统资源信息');
// logger.log('获取Docker状态和系统资源信息');
const [dockerResponse, resourcesResponse] = await Promise.all([
fetch('/api/docker/status').catch(err => { logger.error('Docker status fetch failed:', err); return null; }), // 添加 catch
fetch('/api/system-resources').catch(err => { logger.error('System resources fetch failed:', err); return null; }) // 添加 catch
fetch('/api/docker/status').catch(err => { /* logger.error('Docker status fetch failed:', err); */ return null; }), // 添加 catch
fetch('/api/system-resources').catch(err => { /* logger.error('System resources fetch failed:', err); */ return null; }) // 添加 catch
]);
logger.debug('API响应结果:', { dockerOk: dockerResponse?.ok, resourcesOk: resourcesResponse?.ok });
// logger.debug('API响应结果:', { dockerOk: dockerResponse?.ok, resourcesOk: resourcesResponse?.ok });
let dockerDataArray = null; // 用于存放容器数组
let isDockerServiceRunning = false; // 用于判断 Docker 服务本身是否响应
@@ -53,7 +53,7 @@ async function refreshSystemStatus() {
try {
// 假设 API 直接返回容器数组
dockerDataArray = await dockerResponse.json();
logger.debug('Docker数据:', JSON.stringify(dockerDataArray));
// logger.debug('Docker数据:', JSON.stringify(dockerDataArray));
// 只有当返回的是数组,且状态属性表明 Docker 正在运行时,才认为 Docker 服务是运行的
if (Array.isArray(dockerDataArray)) {
@@ -69,19 +69,19 @@ async function refreshSystemStatus() {
// 只有在没有这两种特定错误时,才认为 Docker 服务正常
isDockerServiceRunning = !hasDockerUnavailableError && !hasContainerListError;
logger.debug(`Docker服务状态: ${isDockerServiceRunning ? '运行中' : '未运行'}, 错误状态:`,
{ hasDockerUnavailableError, hasContainerListError });
// logger.debug(`Docker服务状态: ${isDockerServiceRunning ? '运行中' : '未运行'}, 错误状态:`,
// { hasDockerUnavailableError, hasContainerListError });
} else {
logger.warn('Docker数据不是数组:', typeof dockerDataArray);
// logger.warn('Docker数据不是数组:', typeof dockerDataArray);
isDockerServiceRunning = false;
}
} catch (jsonError) {
logger.error('解析Docker数据失败:', jsonError);
// logger.error('解析Docker数据失败:', jsonError);
dockerDataArray = []; // 解析失败视为空数组
isDockerServiceRunning = false; // JSON 解析失败,认为服务有问题
}
} else {
logger.warn('获取Docker状态失败');
// logger.warn('获取Docker状态失败');
dockerDataArray = []; // 请求失败视为空数组
isDockerServiceRunning = false; // 请求失败,认为服务未运行
}
@@ -91,15 +91,15 @@ async function refreshSystemStatus() {
try {
// --- 添加日志:打印原始响应文本 ---
const resourcesText = await resourcesResponse.text();
logger.debug('原始系统资源响应:', resourcesText);
// logger.debug('原始系统资源响应:', resourcesText);
resourcesData = JSON.parse(resourcesText); // 解析文本
logger.debug('解析后的系统资源数据:', resourcesData);
// logger.debug('解析后的系统资源数据:', resourcesData);
} catch (jsonError) {
logger.error('解析系统资源数据失败:', jsonError);
// logger.error('解析系统资源数据失败:', jsonError);
resourcesData = { cpu: null, memory: null, diskSpace: null };
}
} else {
logger.warn(`获取系统资源失败, 状态: ${resourcesResponse?.status}`);
// logger.warn(`获取系统资源失败, 状态: ${resourcesResponse?.status}`);
resourcesData = { cpu: null, memory: null, diskSpace: null };
}
@@ -121,9 +121,9 @@ async function refreshSystemStatus() {
// --- 修改:将合并后的数据存入缓存 ---
currentSystemData = combinedData; // 缓存数据
logger.debug('合并后的状态数据:', currentSystemData);
// logger.debug('合并后的状态数据:', currentSystemData);
updateSystemStatusUI(currentSystemData);
logger.log('系统状态加载完成');
// logger.log('系统状态加载完成');
// 只在仪表盘页面(且登录框未显示时)才显示成功通知
const adminContainer = document.querySelector('.admin-container');
@@ -144,7 +144,7 @@ async function refreshSystemStatus() {
});
}
} catch (error) {
logger.error('刷新系统状态出错:', error);
// logger.error('刷新系统状态出错:', error);
showSystemStatusError(error.message);
showDashboardError(error.message);
@@ -171,7 +171,7 @@ async function refreshSystemStatus() {
// 显示系统状态错误
function showSystemStatusError(message) {
console.error('系统状态错误:', message);
// console.error('系统状态错误:', message);
// 更新Docker容器表格状态为错误
const containerBody = document.getElementById('dockerContainersBody');
@@ -250,16 +250,16 @@ function updateSystemStatusUI(data) {
try {
// 使用可选链和确保 parseDiskSpace 返回有效对象
const diskData = parseDiskSpace(data.diskSpace);
console.log('[systemStatus] Parsed disk data for progress bar:', diskData);
// console.log('[systemStatus] Parsed disk data for progress bar:', diskData);
if (diskData && typeof diskData.usagePercent === 'number') {
updateProgressBar('diskSpaceProgress', diskData.usagePercent);
console.log(`[systemStatus] Updated disk progress bar with: ${diskData.usagePercent}%`);
// console.log(`[systemStatus] Updated disk progress bar with: ${diskData.usagePercent}%`);
} else {
updateProgressBar('diskSpaceProgress', 0); // 出错或无数据时归零
console.log('[systemStatus] Disk data invalid or missing usagePercent for progress bar.');
// console.log('[systemStatus] Disk data invalid or missing usagePercent for progress bar.');
}
} catch (e) {
console.warn('[systemStatus] Failed to update disk progress bar:', e);
// console.warn('[systemStatus] Failed to update disk progress bar:', e);
updateProgressBar('diskSpaceProgress', 0); // 出错时归零
}
@@ -269,13 +269,13 @@ function updateSystemStatusUI(data) {
const memPercent = data.memory?.usedPercentage;
if (typeof memPercent === 'number') {
updateProgressBar('memoryProgress', memPercent);
console.log(`[systemStatus] Updated memory progress bar with: ${memPercent}%`);
// console.log(`[systemStatus] Updated memory progress bar with: ${memPercent}%`);
} else {
updateProgressBar('memoryProgress', 0);
console.log('[systemStatus] Memory data invalid or missing usedPercentage for progress bar.');
// console.log('[systemStatus] Memory data invalid or missing usedPercentage for progress bar.');
}
} catch (e) {
console.warn('[systemStatus] Failed to update memory progress bar:', e);
// console.warn('[systemStatus] Failed to update memory progress bar:', e);
updateProgressBar('memoryProgress', 0);
}
@@ -285,22 +285,22 @@ function updateSystemStatusUI(data) {
const cpuUsage = data.cpu?.usage;
if (typeof cpuUsage === 'number') {
updateProgressBar('cpuProgress', cpuUsage);
console.log(`[systemStatus] Updated CPU progress bar with: ${cpuUsage}%`);
// console.log(`[systemStatus] Updated CPU progress bar with: ${cpuUsage}%`);
} else {
updateProgressBar('cpuProgress', 0);
console.log('[systemStatus] CPU data invalid or missing usage for progress bar.');
// console.log('[systemStatus] CPU data invalid or missing usage for progress bar.');
}
} catch (e) {
console.warn('[systemStatus] Failed to update CPU progress bar:', e);
// console.warn('[systemStatus] Failed to update CPU progress bar:', e);
updateProgressBar('cpuProgress', 0);
}
console.log('[systemStatus] UI update process finished.');
// console.log('[systemStatus] UI update process finished.');
}
// 显示系统状态加载
function showSystemStatusLoading() {
console.log('显示系统状态加载中...');
// console.log('显示系统状态加载中...');
// 更新Docker容器表格状态为加载中
const containerTable = document.getElementById('dockerContainersTable');
@@ -348,7 +348,7 @@ function updateProgressBar(id, percentage) {
if (bar) {
// 确保 percentage 是有效的数字,否则设为 0
const validPercentage = (typeof percentage === 'number' && !isNaN(percentage)) ? Math.max(0, Math.min(100, percentage)) : 0;
console.log(`[systemStatus] Setting progress bar ${id} to ${validPercentage}%`);
// console.log(`[systemStatus] Setting progress bar ${id} to ${validPercentage}%`);
bar.style.width = `${validPercentage}%`;
bar.setAttribute('aria-valuenow', validPercentage); // 更新 aria 属性
@@ -361,7 +361,7 @@ function updateProgressBar(id, percentage) {
bar.className = 'progress-bar bg-danger';
}
} else {
console.warn(`[systemStatus] Progress bar element with id '${id}' not found.`);
// console.warn(`[systemStatus] Progress bar element with id '${id}' not found.`);
}
}
@@ -409,7 +409,7 @@ function showDetailsDialog(type) {
}
});
} catch (error) {
console.error('显示详情对话框失败:', error);
// console.error('显示详情对话框失败:', error);
core.showAlert('加载详情时出错: ' + error.message, 'error');
}
}
@@ -545,7 +545,7 @@ function generateDiskDetailsHtml(diskData) {
// 更新Docker状态指示器
function updateDockerStatus(available) {
console.log(`[systemStatus] Updating top Docker status indicator to: ${available ? 'running' : 'stopped'}`);
// console.log(`[systemStatus] Updating top Docker status indicator to: ${available ? 'running' : 'stopped'}`);
const statusIndicator = document.getElementById('dockerStatusIndicator'); // 假设这是顶部指示器的 ID
const statusText = document.getElementById('dockerStatusText'); // 假设这是顶部文本的 ID
@@ -668,26 +668,43 @@ function initDashboard() {
// 初始加载系统状态
refreshSystemStatus(); // <-- 调用确保加载数据
console.log('仪表板初始化完成');
// console.log('仪表板初始化完成');
}
// 创建仪表板卡片
function createDashboardCard(data) {
function createDashboardCard(cardInfo) {
const card = document.createElement('div');
card.className = 'dashboard-card';
card.id = `${data.id}-card`;
card.id = `${cardInfo.id}-card`;
// Icon: Use cardInfo.icon directly as it should contain the full class string (e.g., "fas fa-microchip")
const iconHtml = cardInfo.icon ? `<i class="${cardInfo.icon}"></i>` : '';
// Action: Determine if it's a link or a callback
let actionHtml = '';
if (cardInfo.actionLink) {
actionHtml = `<a href="${cardInfo.actionLink}" class="card-action">${cardInfo.actionText || '查看'}</a>`;
} else if (cardInfo.actionCallback && typeof cardInfo.actionCallback === 'function') {
// Register the callback and create a link to call handleCardAction
window.systemStatus.cardActionCallbacks[cardInfo.id] = cardInfo.actionCallback;
actionHtml = `<a href="javascript:void(0)" class="card-action" onclick="window.systemStatus.handleCardAction('${cardInfo.id}')">${cardInfo.actionText || '操作'}</a>`;
} else if (cardInfo.actionText) {
// Fallback if only actionText is provided (no specific action)
actionHtml = `<span class="card-action is-text">${cardInfo.actionText}</span>`;
}
card.innerHTML = `
<div class="card-icon">
<i class="fas ${data.icon}"></i>
<div class="card-icon" style="background-color: ${cardInfo.color || 'var(--primary-light)'}; color: ${cardInfo.color ? 'white' : 'var(--primary-color)'};">
${iconHtml}
</div>
<h3 class="card-title">${data.title}</h3>
<div class="card-value" id="${data.id}-value">${data.value}</div>
<div class="card-description">${data.description}</div>
<div class="card-footer">
<div class="trend ${data.trend}" id="${data.id}-trend"></div>
<div class="card-action" onclick="systemStatus.showDetailsDialog('${data.id}')">${data.action}</div>
<div class="card-content">
<h3 class="card-title">${cardInfo.title || '未命名卡片'}</h3>
<div class="card-value ${cardInfo.valueClass || ''}" id="${cardInfo.id}-value">
${cardInfo.value !== undefined ? cardInfo.value : '--'} ${cardInfo.unit || ''}
</div>
${cardInfo.description ? `<div class="card-description">${cardInfo.description}</div>` : ''}
</div>
${actionHtml ? `<div class="card-footer">${actionHtml}</div>` : ''}
`;
return card;
@@ -695,190 +712,175 @@ function createDashboardCard(data) {
// 更新仪表板卡片
function updateDashboardCards(data) {
logger.debug('更新仪表板卡片:', data);
if (!data) {
logger.error('仪表板数据为空');
logger.debug('Updating dashboard cards with data:', data);
const dashboardGrid = document.querySelector('.dashboard-grid');
if (!dashboardGrid) {
logger.warn('Dashboard grid not found. Cannot update cards.');
return;
}
// 清空旧的回调
window.systemStatus.cardActionCallbacks = {};
// 确保有 dockerContainers 数据
const containers = Array.isArray(data.dockerContainers) ? data.dockerContainers : [];
// 更新容器数量卡片
const containersValue = document.getElementById('containers-value');
if (containersValue) {
// 确保 dockerContainers 是数组
containersValue.textContent = Array.isArray(data?.dockerContainers) ? data.dockerContainers.length : '--';
logger.debug(`容器数量卡片更新为: ${containersValue.textContent}`);
}
// 更新内存使用卡片
const memoryValue = document.getElementById('memory-value');
if (memoryValue) {
let memPercent = null;
logger.debug(`内存数据:`, data.memory);
// 特别处理兼容层返回的数据格式
if (data.memory && typeof data.memory === 'object') {
// 首先检查是否有 percent 属性
if (typeof data.memory.percent === 'string') {
memPercent = parseFloat(data.memory.percent.replace('%', ''));
}
// 如果 percent 不存在或无效,尝试从 total 和 used 计算
else if (typeof data.memory.total === 'number' && typeof data.memory.used === 'number' && data.memory.total > 0) {
memPercent = (data.memory.used / data.memory.total) * 100;
}
// 将单位转换为更易读的格式
const totalMemory = formatByteSize(data.memory.total);
const usedMemory = formatByteSize(data.memory.used);
const freeMemory = formatByteSize(data.memory.free);
// 更新内存详情表格中的值
updateMemoryDetailsTable(totalMemory, usedMemory, freeMemory, memPercent);
}
memoryValue.textContent = (typeof memPercent === 'number' && !isNaN(memPercent))
? `${memPercent.toFixed(1)}%` // 保留一位小数
: '未知';
logger.debug(`内存卡片更新为: ${memoryValue.textContent}`);
// 更新内存进度条
const memoryProgressBar = document.getElementById('memory-progress');
if (memoryProgressBar && typeof memPercent === 'number' && !isNaN(memPercent)) {
memoryProgressBar.style.width = `${memPercent}%`;
if (memPercent > 90) {
memoryProgressBar.className = 'progress-bar bg-danger';
} else if (memPercent > 70) {
memoryProgressBar.className = 'progress-bar bg-warning';
} else {
memoryProgressBar.className = 'progress-bar bg-success';
}
}
}
// 更新CPU负载卡片
const cpuValue = document.getElementById('cpu-value');
if (cpuValue) {
let cpuUsage = null;
// 特别处理兼容层返回的CPU数据
if (data.cpu && typeof data.cpu === 'object') {
logger.debug(`CPU数据:`, data.cpu);
// 如果有loadAvg数组使用第一个值1分钟平均负载
if (Array.isArray(data.cpu.loadAvg) && data.cpu.loadAvg.length > 0) {
// 负载需要除以核心数来计算百分比
const cores = data.cpu.cores || 1;
cpuUsage = (data.cpu.loadAvg[0] / cores) * 100;
// 防止超过100%
cpuUsage = Math.min(cpuUsage, 100);
// 更新CPU详情表格
updateCpuDetailsTable(data.cpu.cores, data.cpu.model, data.cpu.speed, cpuUsage);
}
}
cpuValue.textContent = (typeof cpuUsage === 'number' && !isNaN(cpuUsage))
? `${cpuUsage.toFixed(1)}%` // 保留一位小数
: '未知';
logger.debug(`CPU卡片更新为: ${cpuValue.textContent}`);
// 更新CPU进度条
const cpuProgressBar = document.getElementById('cpu-progress');
if (cpuProgressBar && typeof cpuUsage === 'number' && !isNaN(cpuUsage)) {
cpuProgressBar.style.width = `${cpuUsage}%`;
if (cpuUsage > 90) {
cpuProgressBar.className = 'progress-bar bg-danger';
} else if (cpuUsage > 70) {
cpuProgressBar.className = 'progress-bar bg-warning';
} else {
cpuProgressBar.className = 'progress-bar bg-success';
}
}
}
// 更新磁盘空间卡片
const diskValue = document.getElementById('disk-value');
const diskTrend = document.getElementById('disk-trend');
if (diskValue) {
let diskPercent = null;
if (data.disk && typeof data.disk === 'object') {
logger.debug('磁盘数据:', data.disk);
// 特别处理直接从df命令返回的格式
if (data.disk.percent) {
// 如果percent属性存在直接使用
if (typeof data.disk.percent === 'string') {
diskPercent = parseFloat(data.disk.percent.replace('%', ''));
} else if (typeof data.disk.percent === 'number') {
diskPercent = data.disk.percent;
// 过滤掉错误指示对象和没有有效ID的容器
const validContainers = containers.filter(c => c && c.id && c.id !== 'n/a' && !c.error);
const runningContainersCount = validContainers.filter(c => c.state && typeof c.state === 'string' && c.state.toLowerCase().includes('running')).length;
const totalValidContainersCount = validContainers.length;
logger.debug('Valid containers for dashboard:', validContainers);
logger.debug(`Running: ${runningContainersCount}, Total Valid: ${totalValidContainersCount}`);
const cardsData = [
{
id: 'dockerStatus',
title: 'Docker 服务',
value: data.dockerStatus === 'running' ? '运行中' : '已停止',
description: data.dockerStatus === 'running' ? '服务连接正常' : '服务未连接或不可用',
icon: 'fab fa-docker',
color: data.dockerStatus === 'running' ? 'var(--success-color)' : 'var(--danger-color)',
actionText: data.dockerStatus === 'running' ? '查看容器' : null,
actionLink: data.dockerStatus === 'running' ? '#docker-status' : null,
valueClass: data.dockerStatus === 'running' ? 'text-success' : 'text-danger'
},
{
id: 'containersCount',
title: '运行中容器',
value: runningContainersCount,
description: `总容器数: ${totalValidContainersCount}`,
icon: 'fas fa-box-open',
color: 'var(--primary-color)',
actionText: '管理容器',
actionCallback: () => {
if (typeof dockerManager !== 'undefined' && typeof dockerManager.showContainersPage === 'function') {
core.navigateToSection('docker-status');
} else if (typeof dockerManager !== 'undefined' && typeof dockerManager.showManagementModal === 'function') {
dockerManager.showManagementModal();
} else {
window.location.hash = 'docker-status';
if (window.app && typeof window.app.handleNavigation === 'function') {
window.app.handleNavigation('docker-status');
} else if (core && typeof core.navigateToSection === 'function') {
core.navigateToSection('docker-status');
} else {
console.warn('No function found to display Docker management (modal or section).');
const sidebarItem = document.querySelector('li[data-section="docker-status"]');
if (sidebarItem) sidebarItem.click();
else alert('容器管理功能导航失败');
}
}
// 更新磁盘详情表格
updateDiskDetailsTable(
data.disk.size || "未知",
data.disk.used || "未知",
data.disk.available || "未知",
diskPercent
);
}
} else if (data.diskSpace && typeof data.diskSpace === 'object') {
logger.debug('使用旧磁盘数据格式:', data.diskSpace);
// 兼容老的diskSpace字段
if (data.diskSpace.percent) {
if (typeof data.diskSpace.percent === 'string') {
diskPercent = parseFloat(data.diskSpace.percent.replace('%', ''));
} else if (typeof data.diskSpace.percent === 'number') {
diskPercent = data.diskSpace.percent;
},
unit: '个'
},
{
id: 'cpuUsage',
title: 'CPU 使用率',
value: (() => {
if (data.cpu) {
if (typeof data.cpu.usage === 'number' && !isNaN(data.cpu.usage)) return data.cpu.usage.toFixed(1) + '%';
if (typeof data.cpu.currentLoad === 'number' && !isNaN(data.cpu.currentLoad)) return data.cpu.currentLoad.toFixed(1) + '%';
if (typeof data.cpu.load === 'number' && !isNaN(data.cpu.load)) return data.cpu.load.toFixed(1) + '%';
if (data.cpu.currentLoad && typeof data.cpu.currentLoad.avgload === 'number' && !isNaN(data.cpu.currentLoad.avgload)) return data.cpu.currentLoad.avgload.toFixed(1) + '%';
// Add logic to calculate from loadAvg, similar to generateCpuDetailsHtml
if (Array.isArray(data.cpu.loadAvg) && data.cpu.loadAvg.length > 0 && typeof data.cpu.loadAvg[0] === 'number' && !isNaN(data.cpu.loadAvg[0])) {
const cores = data.cpu.cores || 1;
const cpuUsageFromLoadAvg = (data.cpu.loadAvg[0] / cores) * 100;
return Math.min(cpuUsageFromLoadAvg, 100).toFixed(1) + '%';
}
}
// 更新磁盘详情表格
updateDiskDetailsTable(
data.diskSpace.size || "未知",
data.diskSpace.used || "未知",
data.diskSpace.available || "未知",
diskPercent
);
return 'N/A';
})(),
description: `核心数: ${data.cpu ? (data.cpu.cores || 'N/A') : 'N/A'}`,
icon: 'fas fa-microchip',
color: 'var(--warning-color)',
actionText: '详细信息',
actionCallback: () => showDetailsDialog('cpu'),
unit: ''
},
{
id: 'memoryUsage',
title: '内存使用率',
value: (() => {
if (data.memory) {
// 优先基于 active 内存计算百分比 (更接近真实程序占用)
if (typeof data.memory.active === 'number' && typeof data.memory.total === 'number' && data.memory.total > 0 && !isNaN(data.memory.active)) {
return ((data.memory.active / data.memory.total) * 100).toFixed(1) + '%';
}
// 如果后端直接提供了基于某种标准计算的 usedPercentage且 active 不可用,则使用它
if (typeof data.memory.usedPercentage === 'number' && !isNaN(data.memory.usedPercentage)) {
return data.memory.usedPercentage.toFixed(1) + '%';
}
// 最后,如果 active 和 usedPercentage 都没有,才基于 used 计算 (这可能包含缓存)
if (typeof data.memory.used === 'number' && typeof data.memory.total === 'number' && data.memory.total > 0 && !isNaN(data.memory.used)) {
return ((data.memory.used / data.memory.total) * 100).toFixed(1) + '%';
}
}
return 'N/A';
})(),
description: `已用: ${data.memory && typeof data.memory.used === 'number' && !isNaN(data.memory.used) ? formatByteSize(data.memory.used) : 'N/A'} / 总量: ${data.memory && data.memory.total ? formatByteSize(data.memory.total) : 'N/A'}`,
icon: 'fas fa-memory',
color: 'var(--info-color)',
actionText: '详细信息',
actionCallback: () => showDetailsDialog('memory'),
unit: ''
},
{
id: 'diskUsage',
title: '磁盘使用率',
value: data.disk && data.disk.percent ? data.disk.percent : 'N/A',
description: `可用: ${data.disk && data.disk.available ? data.disk.available : 'N/A'} / 总量: ${data.disk && data.disk.size ? data.disk.size : 'N/A'}`,
icon: 'fas fa-hdd',
color: 'var(--secondary-color)',
actionText: '详细信息',
actionCallback: () => showDetailsDialog('disk'),
unit: ''
},
{
id: 'uptime', // 假设有一个API可以获取系统启动时间
title: '系统运行时长',
value: data.uptime || 'N/A', // 从 data.uptime 获取
description: '系统持续运行时间',
icon: 'fas fa-clock',
color: 'var(--purple-color)', // 自定义颜色变量
actionText: '系统日志',
actionLink: '#system-logs', // 假设有系统日志页面
unit: ''
}
];
dashboardGrid.innerHTML = ''; // 清空现有卡片
cardsData.forEach(cardInfo => {
if ((cardInfo.id === 'uptime' && !data.uptime) && cardInfo.id !== 'dockerStatus' && cardInfo.id !== 'containersCount') {
// 如果是 uptime卡片且没有uptime数据则跳过 (但保留 Docker 和容器数量卡片)
if (cardInfo.id !== 'cpuUsage' && cardInfo.id !== 'memoryUsage' && cardInfo.id !== 'diskUsage') {
logger.debug(`Skipping card ${cardInfo.id} due to missing data or specific condition.`);
return;
}
}
// 特殊处理 Docker 服务卡片,即使服务未运行也显示
if (cardInfo.id === 'dockerStatus') {
// 总是创建 Docker 状态卡片
} else if (data.dockerStatus !== 'running' &&
(cardInfo.id === 'containersCount')) {
// 如果Docker服务未运行则只显示 "N/A" 或 0 对于容器数量
cardInfo.value = 0; // 或 'N/A'
cardInfo.description = 'Docker 服务未运行';
} else if (data.dockerStatus !== 'running' && cardInfo.id !== 'uptime') {
// 如果 Docker 服务未运行,并且不是 uptime 卡片,则跳过其他依赖 Docker 的卡片
if (cardInfo.id !== 'cpuUsage' && cardInfo.id !== 'memoryUsage' && cardInfo.id !== 'diskUsage') {
logger.debug(`Skipping card ${cardInfo.id} because Docker is not running.`);
return;
}
}
if (typeof diskPercent === 'number' && !isNaN(diskPercent)) {
diskValue.textContent = `${diskPercent.toFixed(0)}%`; // 磁盘百分比通常不带小数
logger.debug(`磁盘卡片更新为: ${diskValue.textContent}`);
// 更新磁盘进度条
const diskProgressBar = document.getElementById('disk-progress');
if (diskProgressBar) {
diskProgressBar.style.width = `${diskPercent}%`;
if (diskPercent > 90) {
diskProgressBar.className = 'progress-bar bg-danger';
} else if (diskPercent > 70) {
diskProgressBar.className = 'progress-bar bg-warning';
} else {
diskProgressBar.className = 'progress-bar bg-success';
}
}
// 更新趋势信息
if (diskTrend) {
if (diskPercent > 90) {
diskTrend.className = 'trend down text-danger';
diskTrend.innerHTML = '<i class="fas fa-exclamation-triangle"></i> 磁盘空间不足';
} else if (diskPercent > 75) {
diskTrend.className = 'trend down text-warning';
diskTrend.innerHTML = '<i class="fas fa-arrow-up"></i> 磁盘使用率较高';
} else {
diskTrend.className = 'trend up text-success';
diskTrend.innerHTML = '<i class="fas fa-check-circle"></i> 磁盘空间充足';
}
}
} else {
diskValue.textContent = '未知';
if(diskTrend) diskTrend.innerHTML = '';
logger.debug(`磁盘卡片值为未知,无效百分比: ${diskPercent}`);
}
}
const cardElement = createDashboardCard(cardInfo);
dashboardGrid.appendChild(cardElement);
});
}
// 格式化字节大小为易读格式
@@ -999,13 +1001,24 @@ function formatTime(timestamp) {
// 显示仪表盘加载状态
function showDashboardLoading() {
const cards = ['containers', 'memory', 'cpu', 'disk'];
cards.forEach(id => {
const valueElement = document.getElementById(`${id}-value`);
if (valueElement) {
valueElement.innerHTML = '<div class="loading-spinner-small"></div>';
const dashboardGrid = document.querySelector('.dashboard-grid');
if (dashboardGrid) {
// Instead of targeting specific value elements, show a general loading message
// or a spinner within the grid itself if it's empty.
// For now, we'll rely on the card creation process to populate it.
// If cards are not yet defined, we can show a message.
if (dashboardGrid.children.length === 0) {
dashboardGrid.innerHTML = '<div class="loading-message"><div class="loading-spinner-large"></div><p>正在加载控制面板数据...</p></div>';
}
});
}
// The old method below targets specific value elements which might not exist before cards are drawn.
// const cards = ['dockerStatus', 'containersCount', 'cpuUsage', 'memoryUsage', 'diskUsage', 'uptime'];
// cards.forEach(id => {
// const valueElement = document.getElementById(`${id}-value`);
// if (valueElement) {
// valueElement.innerHTML = '<div class="loading-spinner-small"></div>';
// }
// });
}
// 显示仪表盘错误
@@ -1060,6 +1073,18 @@ window.systemStatus.initDashboard = initDashboard;
// 暴露调试设置函数,方便开发时打开调试
window.systemStatus.setDebug = logger.setDebug;
// --- 新增:用于存储卡片操作回调 ---
window.systemStatus.cardActionCallbacks = {};
// --- 新增:处理卡片操作的通用函数 ---
window.systemStatus.handleCardAction = function(cardId) {
if (window.systemStatus.cardActionCallbacks[cardId]) {
window.systemStatus.cardActionCallbacks[cardId]();
} else {
console.warn(`No action callback registered for card ID: ${cardId}`);
}
};
/* 添加一些基础样式到 CSS (如果 web/style.css 不可用,这里会失败) */
/* 理想情况下,这些样式应该放在 web/style.css */
const customHelpStyles = `

View File

@@ -11,24 +11,24 @@ async function getUserInfo() {
if (!sessionData.authenticated) {
// 用户未登录,不显示错误,静默返回
console.log('用户未登录或会话无效,跳过获取用户信息');
// console.log('用户未登录或会话无效,跳过获取用户信息');
return;
}
// 用户已登录,获取用户信息
console.log('会话有效,尝试获取用户信息...');
// console.log('会话有效,尝试获取用户信息...');
const response = await fetch('/api/user-info');
if (!response.ok) {
// 检查是否是认证问题
if (response.status === 401) {
console.log('会话已过期,需要重新登录');
// console.log('会话已过期,需要重新登录');
return;
}
throw new Error('获取用户信息失败');
}
const data = await response.json();
console.log('获取到用户信息:', data);
// console.log('获取到用户信息:', data);
// 更新顶部导航栏的用户名
const currentUsername = document.getElementById('currentUsername');
@@ -58,7 +58,7 @@ async function getUserInfo() {
}
}
} catch (error) {
console.error('获取用户信息失败:', error);
// console.error('获取用户信息失败:', error);
// 不显示错误通知,只在控制台记录错误
}
}
@@ -146,12 +146,12 @@ async function changePassword(event) {
}).then((result) => {
// 当计时器结束或弹窗被关闭时 (包括点击确定按钮)
if (result.dismiss === Swal.DismissReason.timer || result.isConfirmed) {
console.log('计时器结束或手动确认,执行登出');
// console.log('计时器结束或手动确认,执行登出');
auth.logout();
}
});
} catch (error) {
console.error('修改密码失败:', error);
// console.error('修改密码失败:', error);
core.showAlert('修改密码失败: ' + error.message, 'error');
}
}
@@ -219,7 +219,7 @@ function checkUcPasswordStrength() {
// 初始化用户中心
function initUserCenter() {
console.log('初始化用户中心');
// console.log('初始化用户中心');
// 获取用户信息
getUserInfo();
@@ -241,7 +241,7 @@ function loadUserStats() {
// 导出模块
const userCenter = {
init: function() {
console.log('初始化用户中心模块...');
// console.log('初始化用户中心模块...');
// 可以在这里调用初始化逻辑,也可以延迟到需要时调用
return Promise.resolve(); // 返回一个已解决的 Promise保持与其他模块一致
},

View File

@@ -531,27 +531,6 @@
transform: translateY(0); /* 覆盖默认的按钮hover效果 */
}
.copy-success {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 0.75rem 1.5rem;
border-radius: var(--radius-md);
z-index: 1000;
animation: fadeInOut 1.5s ease-in-out;
pointer-events: none;
}
@keyframes fadeInOut {
0% { opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; }
}
/* 标签展示区域 - 修复宽度与主容器一致 */
#imageTagsView {
width: 100%;
@@ -1012,27 +991,6 @@
transform: translateY(0); /* 覆盖默认的按钮hover效果 */
}
.copy-success {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 0.75rem 1.5rem;
border-radius: var(--radius-md);
z-index: 1000;
animation: fadeInOut 1.5s ease-in-out;
pointer-events: none;
}
@keyframes fadeInOut {
0% { opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; }
}
/* 标签展示区域 - 修复宽度与主容器一致 */
#imageTagsView {
width: 100%;
@@ -1840,17 +1798,32 @@
z-index: 1100;
animation: fadeIn 0.3s ease-in;
text-align: center;
max-width: 80%;
max-width: 90%;
display: flex;
align-items: center;
gap: 0.75rem;
}
.toast-notification i {
font-size: 1.2rem;
}
.toast-notification.error {
border-left: 4px solid var(--danger-color);
}
.toast-notification.error i {
color: var(--danger-color);
}
.toast-notification.info {
border-left: 4px solid var(--info-color);
}
.toast-notification.info i {
color: var(--info-color);
}
.toast-notification.fade-out {
opacity: 0;
transition: opacity 0.3s ease-out;
@@ -1997,38 +1970,56 @@
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: var(--container-bg);
color: var(--text-primary);
padding: 1rem 2rem;
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
/* Glassmorphism styles - Simplified & Reinforced */
background-color: rgba(250, 250, 250, 0.4); /* 调整了Alpha值使其更透明一些 */
backdrop-filter: blur(12px); /* 调整模糊值 */
-webkit-backdrop-filter: blur(12px); /* Safari 兼容 */
border: 1px solid rgba(255, 255, 255, 0.2); /* 边框更柔和 */
color: #2A3749; /* 使用了您CSS变量中的 --text-primary */
padding: 1rem 1.5rem;
border-radius: 12px; /* 圆角调整 */
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); /* 调整阴影 */
z-index: 1100;
animation: fadeIn 0.3s ease-in;
animation: fadeIn 0.3s ease-in-out; /* 使用 ease-in-out */
text-align: center;
max-width: 90%;
min-width: 250px; /* 最小宽度 */
max-width: 350px; /* 最大宽度,避免过宽 */
display: flex;
align-items: center;
gap: 0.75rem;
gap: 0.8rem; /* 图标与文字间距 */
}
.toast-notification i {
font-size: 1.2rem;
font-size: 1.25rem; /* 图标大小 */
color: inherit; /* 默认继承文本颜色 */
}
/* 状态特定样式 */
.toast-notification.success {
/* background-color: rgba(230, 245, 230, 0.4); /* 可选:为成功状态叠加一层淡淡的绿色玻璃感 */
/* border-left: none !important; /* 强制移除任何可能的旧左边框 */
}
.toast-notification.success i {
color: var(--success-color); /* 使用CSS变量 */
}
.toast-notification.error {
border-left: 4px solid var(--danger-color);
/* background-color: rgba(250, 230, 230, 0.4); /* 可选:为错误状态叠加一层淡淡的红色玻璃感 */
/* border-left: none !important; */
}
.toast-notification.error i {
color: var(--danger-color);
color: var(--danger-color); /* 使用CSS变量 */
}
.toast-notification.info {
border-left: 4px solid var(--info-color);
/* background-color: rgba(230, 240, 250, 0.4); /* 可选:为信息状态叠加一层淡淡的蓝色玻璃感 */
/* border-left: none !important; */
}
.toast-notification.info i {
color: var(--info-color);
color: var(--info-color); /* 使用CSS变量 */
}
/* 优化标签表格性能 */
@@ -2104,50 +2095,6 @@
box-shadow: none;
}
/* 更新toast通知样式 */
.toast-notification {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: var(--container-bg);
color: var(--text-primary);
padding: 1rem 2rem;
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
z-index: 1100;
animation: fadeIn 0.3s ease-in;
text-align: center;
max-width: 90%;
display: flex;
align-items: center;
gap: 0.75rem;
}
.toast-notification i {
font-size: 1.2rem;
}
.toast-notification.error {
border-left: 4px solid var(--danger-color);
}
.toast-notification.error i {
color: var(--danger-color);
}
.toast-notification.info {
border-left: 4px solid var(--info-color);
}
.toast-notification.info i {
color: var(--info-color);
}
.toast-notification.fade-out {
opacity: 0;
transition: opacity 0.3s ease-out;
}
/* 添加标签加载进度样式 */
.loading-progress {
@@ -2643,7 +2590,7 @@
/* 为 操作 列设置宽度 */
#dockerStatusTable th:nth-child(5),
#dockerStatusTable td:nth-child(5) {
width: 150px; /* 或 20% */
width: 20%;
text-align: center; /* 操作按钮居中 */
}
@@ -2982,4 +2929,10 @@
#dockerStatusTable th:nth-child(5),
#dockerStatusTable td:nth-child(5) {
width: 20%;
}
/* 新增:使教程页面的代码块在悬停时也显示复制按钮 */
#documentationText pre:hover .copy-btn,
.doc-content pre:hover .copy-btn {
opacity: 1;
}