|
@@ -416,9 +416,64 @@
|
|
|
border-top: 3px solid #0366d6;
|
|
|
animation: spin 1s linear infinite;
|
|
|
}
|
|
|
+
|
|
|
+ .loading-spinner {
|
|
|
+ position: fixed;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ width: 50px;
|
|
|
+ height: 50px;
|
|
|
+ border: 5px solid #f3f3f3;
|
|
|
+ border-top: 5px solid #3498db;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ z-index: 9999;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes spin {
|
|
|
+ 0% { transform: translate(-50%, -50%) rotate(0deg); }
|
|
|
+ 100% { transform: translate(-50%, -50%) rotate(360deg); }
|
|
|
+ }
|
|
|
+
|
|
|
+ .disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .status-cell {
|
|
|
+ position: relative;
|
|
|
+ min-height: 24px; /* 确保单元格有足够的高度来容纳加载动画 */
|
|
|
+ }
|
|
|
+ .loading-container {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading {
|
|
|
+ display: inline-block;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ border: 3px solid rgba(0, 0, 0, 0.1);
|
|
|
+ border-radius: 50%;
|
|
|
+ border-top: 3px solid #0366d6;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+ }
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
+ <div id="loadingSpinner" class="loading-spinner" style="display: none;"></div>
|
|
|
<div id="loadingIndicator" style="display: flex; justify-content: center; align-items: center; height: 100vh;">
|
|
|
<p>加载中...</p>
|
|
|
</div>
|
|
@@ -1318,7 +1373,56 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ async function refreshDockerStatus() {
|
|
|
+ const spinner = document.getElementById('loadingSpinner');
|
|
|
+ const refreshButton = document.getElementById('refreshDockerStatusButton');
|
|
|
+ const table = document.getElementById('dockerStatusTable');
|
|
|
+
|
|
|
+ try {
|
|
|
+ spinner.style.display = 'block';
|
|
|
+ refreshButton.classList.add('disabled');
|
|
|
+ table.classList.add('disabled');
|
|
|
+
|
|
|
+ await loadDockerStatus();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('刷新 Docker 状态失败:', error);
|
|
|
+ alert('刷新 Docker 状态失败: ' + error.message);
|
|
|
+ } finally {
|
|
|
+ spinner.style.display = 'none';
|
|
|
+ refreshButton.classList.remove('disabled');
|
|
|
+ table.classList.remove('disabled');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function saveDockerStatusToCache(containerStatus) {
|
|
|
+ localStorage.setItem('dockerStatus', JSON.stringify(containerStatus));
|
|
|
+ localStorage.setItem('dockerStatusTimestamp', Date.now());
|
|
|
+ }
|
|
|
+
|
|
|
+ function getDockerStatusFromCache() {
|
|
|
+ const cachedStatus = localStorage.getItem('dockerStatus');
|
|
|
+ const timestamp = localStorage.getItem('dockerStatusTimestamp');
|
|
|
+ if (cachedStatus && timestamp) {
|
|
|
+ // 检查缓存是否在过去5分钟内更新过
|
|
|
+ if (Date.now() - parseInt(timestamp) < 5 * 60 * 1000) {
|
|
|
+ return JSON.parse(cachedStatus);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
async function loadDockerStatus() {
|
|
|
+ const tbody = document.getElementById('dockerStatusTableBody');
|
|
|
+
|
|
|
+ // 尝试从缓存加载数据
|
|
|
+ const cachedStatus = getDockerStatusFromCache();
|
|
|
+ if (cachedStatus) {
|
|
|
+ renderDockerStatus(cachedStatus);
|
|
|
+ isDockerStatusLoaded = true;
|
|
|
+ } else if (!isDockerStatusLoaded) {
|
|
|
+ tbody.innerHTML = '<tr><td colspan="8" style="text-align: center;">加载中...</td></tr>';
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
const response = await fetch('/api/docker-status');
|
|
|
if (!response.ok) {
|
|
@@ -1329,21 +1433,22 @@
|
|
|
}
|
|
|
const containerStatus = await response.json();
|
|
|
renderDockerStatus(containerStatus);
|
|
|
+ isDockerStatusLoaded = true;
|
|
|
+ saveDockerStatusToCache(containerStatus);
|
|
|
} catch (error) {
|
|
|
console.error('Error loading Docker status:', error);
|
|
|
- alert('加载 Docker 状态失败: ' + error.message);
|
|
|
- // 清空状态表格并显示错误信息
|
|
|
- const tbody = document.getElementById('dockerStatusTableBody');
|
|
|
- tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: red;">${error.message}</td></tr>`;
|
|
|
+ if (!cachedStatus) {
|
|
|
+ tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: red;">${error.message}</td></tr>`;
|
|
|
+ }
|
|
|
+ isDockerStatusLoaded = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function renderDockerStatus(containerStatus) {
|
|
|
+ const table = document.getElementById('dockerStatusTable');
|
|
|
+ const thead = table.getElementsByTagName('thead')[0];
|
|
|
const tbody = document.getElementById('dockerStatusTableBody');
|
|
|
- tbody.innerHTML = '';
|
|
|
-
|
|
|
- // 添加表头
|
|
|
- const thead = document.getElementById('dockerStatusTable').getElementsByTagName('thead')[0];
|
|
|
+
|
|
|
thead.innerHTML = `
|
|
|
<tr>
|
|
|
<th>容器 ID</th>
|
|
@@ -1357,19 +1462,27 @@
|
|
|
</tr>
|
|
|
`;
|
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
+
|
|
|
containerStatus.forEach(container => {
|
|
|
const row = `
|
|
|
<tr>
|
|
|
<td>${container.id}</td>
|
|
|
<td>${container.name}</td>
|
|
|
<td>${container.image}</td>
|
|
|
- <td id="status-${container.id}">${container.state}</td>
|
|
|
+ <td class="status-cell" id="status-${container.id}">${container.state}</td>
|
|
|
<td>${container.cpu}</td>
|
|
|
<td>${container.memory}</td>
|
|
|
<td>${container.created}</td>
|
|
|
<td>
|
|
|
- <button onclick="restartContainer('${container.id}')" class="action-btn">重启</button>
|
|
|
- <button onclick="stopContainer('${container.id}')" class="action-btn">停止</button>
|
|
|
+ <select onchange="handleContainerAction('${container.id}', '${container.image}', this.value)" class="action-select">
|
|
|
+ <option value="">选择操作</option>
|
|
|
+ <option value="restart">重启</option>
|
|
|
+ <option value="stop">停止</option>
|
|
|
+ <option value="update">更新</option>
|
|
|
+ <option value="delete">删除</option>
|
|
|
+ <option value="logs">查看日志</option>
|
|
|
+ </select>
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
@@ -1377,12 +1490,115 @@
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ function handleContainerAction(id, image, action) {
|
|
|
+ switch(action) {
|
|
|
+ case 'restart':
|
|
|
+ restartContainer(id);
|
|
|
+ break;
|
|
|
+ case 'stop':
|
|
|
+ stopContainer(id);
|
|
|
+ break;
|
|
|
+ case 'update':
|
|
|
+ updateContainer(id, image);
|
|
|
+ break;
|
|
|
+ case 'logs':
|
|
|
+ viewLogs(id);
|
|
|
+ break;
|
|
|
+ case 'delete':
|
|
|
+ deleteContainer(id);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // 重置选择框
|
|
|
+ document.querySelector(`select[onchange*="${id}"]`).value = "";
|
|
|
+ }
|
|
|
+
|
|
|
+ async function viewLogs(id) {
|
|
|
+ try {
|
|
|
+ // 创建模态框
|
|
|
+ const modal = document.createElement('div');
|
|
|
+ modal.style.position = 'fixed';
|
|
|
+ modal.style.left = '0';
|
|
|
+ modal.style.top = '0';
|
|
|
+ modal.style.width = '100%';
|
|
|
+ modal.style.height = '100%';
|
|
|
+ modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
|
|
|
+ modal.style.display = 'flex';
|
|
|
+ modal.style.justifyContent = 'center';
|
|
|
+ modal.style.alignItems = 'center';
|
|
|
+
|
|
|
+ const content = document.createElement('div');
|
|
|
+ content.style.backgroundColor = 'black';
|
|
|
+ content.style.color = 'white';
|
|
|
+ content.style.padding = '20px';
|
|
|
+ content.style.borderRadius = '5px';
|
|
|
+ content.style.width = '80%';
|
|
|
+ content.style.height = '80%';
|
|
|
+ content.style.display = 'flex';
|
|
|
+ content.style.flexDirection = 'column';
|
|
|
+ content.style.position = 'relative';
|
|
|
+
|
|
|
+ const logContent = document.createElement('pre');
|
|
|
+ logContent.style.flex = '1';
|
|
|
+ logContent.style.overflowY = 'auto';
|
|
|
+ logContent.style.padding = '10px';
|
|
|
+ logContent.style.backgroundColor = '#1e1e1e';
|
|
|
+ logContent.style.color = '#d4d4d4';
|
|
|
+ logContent.style.fontFamily = 'monospace';
|
|
|
+ logContent.style.fontSize = '14px';
|
|
|
+ logContent.style.lineHeight = '1.5';
|
|
|
+ logContent.style.whiteSpace = 'pre-wrap';
|
|
|
+ logContent.style.wordBreak = 'break-all';
|
|
|
+
|
|
|
+ content.appendChild(logContent);
|
|
|
+ modal.appendChild(content);
|
|
|
+ document.body.appendChild(modal);
|
|
|
+
|
|
|
+ // 点击模态框外部关闭
|
|
|
+ modal.addEventListener('click', (e) => {
|
|
|
+ if (e.target === modal) {
|
|
|
+ document.body.removeChild(modal);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 建立WebSocket连接以实时获取日志
|
|
|
+ const ws = new WebSocket(`ws://${window.location.host}/api/docker/logs/${id}`);
|
|
|
+
|
|
|
+ ws.onmessage = (event) => {
|
|
|
+ // 过滤掉不可打印字符
|
|
|
+ const filteredData = event.data.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, '');
|
|
|
+ logContent.textContent += filteredData + '\n';
|
|
|
+ logContent.scrollTop = logContent.scrollHeight;
|
|
|
+ };
|
|
|
+
|
|
|
+ ws.onerror = (error) => {
|
|
|
+ console.error('WebSocket错误:', error);
|
|
|
+ logContent.textContent += '连接错误,无法获取实时日志。\n';
|
|
|
+ };
|
|
|
+
|
|
|
+ ws.onclose = () => {
|
|
|
+ logContent.textContent += '日志连接已关闭。\n';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 当模态框关闭时,关闭WebSocket连接
|
|
|
+ modal.addEventListener('click', (e) => {
|
|
|
+ if (e.target === modal) {
|
|
|
+ ws.close();
|
|
|
+ document.body.removeChild(modal);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('查看日志失败:', error);
|
|
|
+ alert('查看日志失败: ' + error.message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
async function restartContainer(id) {
|
|
|
if (confirm('确定要重启这个容器吗?')) {
|
|
|
try {
|
|
|
const statusCell = document.getElementById(`status-${id}`);
|
|
|
- statusCell.innerHTML = '<div class="loading"></div>';
|
|
|
-
|
|
|
+ statusCell.innerHTML = '<div class="loading-container"><div class="loading"></div></div>';
|
|
|
+
|
|
|
const response = await fetch(`/api/docker/restart/${id}`, { method: 'POST' });
|
|
|
if (response.ok) {
|
|
|
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒,确保状态已更新
|
|
@@ -1431,43 +1647,137 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- function refreshDockerStatus() {
|
|
|
- loadDockerStatus();
|
|
|
- }
|
|
|
|
|
|
- document.addEventListener('DOMContentLoaded', function() {
|
|
|
- const sidebarItems = document.querySelectorAll('.sidebar li');
|
|
|
- const contentSections = document.querySelectorAll('.content-section');
|
|
|
- if (isLoggedIn) {
|
|
|
- initEditor();
|
|
|
+ async function updateContainer(id, currentImage) {
|
|
|
+ const tag = prompt(`请输入 ${currentImage} 的新标签:`, 'latest');
|
|
|
+ if (tag) {
|
|
|
+ try {
|
|
|
+ const statusCell = document.getElementById(`status-${id}`);
|
|
|
+ statusCell.textContent = 'Updating';
|
|
|
+ statusCell.style.color = 'orange';
|
|
|
+
|
|
|
+ const response = await fetch(`/api/docker/update/${id}`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ tag })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ const result = await response.json();
|
|
|
+ alert(result.message || '容器更新成功');
|
|
|
+ } else {
|
|
|
+ const errorData = await response.json();
|
|
|
+ throw new Error(errorData.error || '更新失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('更新容器失败:', error);
|
|
|
+ alert('更新容器失败: ' + error.message);
|
|
|
+ } finally {
|
|
|
+ loadDockerStatus(); // 重新加载容器状态
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- if (isLoggedIn) {
|
|
|
+
|
|
|
+ function refreshDockerStatus() {
|
|
|
+ isDockerStatusLoaded = false;
|
|
|
+ localStorage.removeItem('dockerStatus');
|
|
|
+ localStorage.removeItem('dockerStatusTimestamp');
|
|
|
loadDockerStatus();
|
|
|
}
|
|
|
- function showSection(sectionId) {
|
|
|
- contentSections.forEach(section => {
|
|
|
- if (section.id === sectionId) {
|
|
|
- section.classList.add('active');
|
|
|
- } else {
|
|
|
- section.classList.remove('active');
|
|
|
+
|
|
|
+ async function deleteContainer(id) {
|
|
|
+ if (confirm('确定要删除这个容器吗?此操作不可逆。')) {
|
|
|
+ try {
|
|
|
+ const statusCell = document.getElementById(`status-${id}`);
|
|
|
+ statusCell.textContent = 'Deleting';
|
|
|
+ statusCell.style.color = 'red';
|
|
|
+
|
|
|
+ const response = await fetch(`/api/docker/delete/${id}`, { method: 'POST' });
|
|
|
+ if (response.ok) {
|
|
|
+ alert('容器删除成功');
|
|
|
+ loadDockerStatus(); // 重新加载容器状态
|
|
|
+ } else {
|
|
|
+ const errorData = await response.json();
|
|
|
+ throw new Error(errorData.error || '删除失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('删除容器失败:', error);
|
|
|
+ alert('删除容器失败: ' + error.message);
|
|
|
+ loadDockerStatus(); // 重新加载所有容器状态
|
|
|
}
|
|
|
- });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- sidebarItems.forEach(item => {
|
|
|
- item.addEventListener('click', function() {
|
|
|
- const sectionId = this.getAttribute('data-section');
|
|
|
-
|
|
|
- sidebarItems.forEach(si => si.classList.remove('active'));
|
|
|
- this.classList.add('active');
|
|
|
-
|
|
|
- showSection(sectionId);
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
+ const sidebarItems = document.querySelectorAll('.sidebar li');
|
|
|
+ const contentSections = document.querySelectorAll('.content-section');
|
|
|
+
|
|
|
+ let isDockerStatusLoaded = false;
|
|
|
+
|
|
|
+ function showSection(sectionId) {
|
|
|
+ contentSections.forEach(section => {
|
|
|
+ if (section.id === sectionId) {
|
|
|
+ section.classList.add('active');
|
|
|
+ if (sectionId === 'docker-status') {
|
|
|
+ loadDockerStatus(); // 每次显示 Docker 状态部分时都尝试加载
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ section.classList.remove('active');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ localStorage.setItem('currentSection', sectionId);
|
|
|
+
|
|
|
+ // 更新侧边栏active状态
|
|
|
+ sidebarItems.forEach(item => {
|
|
|
+ if (item.getAttribute('data-section') === sectionId) {
|
|
|
+ item.classList.add('active');
|
|
|
+ } else {
|
|
|
+ item.classList.remove('active');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ sidebarItems.forEach(item => {
|
|
|
+ item.addEventListener('click', function() {
|
|
|
+ const sectionId = this.getAttribute('data-section');
|
|
|
+ showSection(sectionId);
|
|
|
+ });
|
|
|
});
|
|
|
- });
|
|
|
|
|
|
- // 初始化:显示第一个部分
|
|
|
- showSection(sidebarItems[0].getAttribute('data-section'));
|
|
|
- });
|
|
|
+ // 页面加载时检查登录状态
|
|
|
+ window.onload = async function() {
|
|
|
+ try {
|
|
|
+ const response = await fetch('/api/check-session');
|
|
|
+ if (response.ok) {
|
|
|
+ isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
|
|
+ if (isLoggedIn) {
|
|
|
+ loadDocumentList();
|
|
|
+ document.getElementById('adminContainer').style.display = 'flex';
|
|
|
+ await loadConfig();
|
|
|
+ initEditor();
|
|
|
+
|
|
|
+ const lastSection = localStorage.getItem('currentSection') || 'basic-config';
|
|
|
+ showSection(lastSection);
|
|
|
+
|
|
|
+ // 立即尝试加载 Docker 状态
|
|
|
+ loadDockerStatus();
|
|
|
+ } else {
|
|
|
+ document.getElementById('loginModal').style.display = 'flex';
|
|
|
+ refreshCaptcha();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new Error('Session check failed');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error during initialization:', error);
|
|
|
+ localStorage.removeItem('isLoggedIn');
|
|
|
+ document.getElementById('loginModal').style.display = 'flex';
|
|
|
+ refreshCaptcha();
|
|
|
+ } finally {
|
|
|
+ document.getElementById('loadingIndicator').style.display = 'none';
|
|
|
+ }
|
|
|
+ };
|
|
|
+ });
|
|
|
</script>
|
|
|
</body>
|
|
|
-</html>
|
|
|
+</html>
|