Ver código fonte

feat: Add Docker Compose Deployment

dqzboy 1 ano atrás
pai
commit
103b6c36d5
6 arquivos alterados com 150 adições e 90 exclusões
  1. 0 4
      Dockerfile
  2. 17 0
      hubcmdui/config.json
  3. 8 0
      hubcmdui/docker-compose.yaml
  4. 21 4
      hubcmdui/server.js
  5. 76 69
      hubcmdui/web/admin.html
  6. 28 13
      hubcmdui/web/index.html

+ 0 - 4
Dockerfile

@@ -1,15 +1,11 @@
-# 使用官方的 Node.js 运行时镜像作为基础镜像
 FROM node:lts-alpine
-
 # 设置工作目录
 WORKDIR /app
 # 复制项目文件到工作目录
 COPY hubcmdui/ .
 # 安装项目依赖
 RUN npm install
-
 # 暴露应用程序的端口
 EXPOSE 3000
-
 # 运行应用程序
 CMD ["node", "server.js"]

+ 17 - 0
hubcmdui/config.json

@@ -0,0 +1,17 @@
+{
+  "logo": "",
+  "proxyDomain": "dqzboy.github.io",
+  "menuItems": [
+    {
+      "text": "首页",
+      "link": "",
+      "newTab": false
+    },
+    {
+      "text": "项目",
+      "link": "https://github.com/dqzboy/Docker-Proxy",
+      "newTab": true
+    }
+  ],
+  "adImages": []
+}

+ 8 - 0
hubcmdui/docker-compose.yaml

@@ -0,0 +1,8 @@
+services:
+  ## HubCMD UI
+  hubcmd-ui:
+    container_name: hubcmd-ui
+    image: dqzboy/hubcmd-ui:latest
+    restart: always
+    ports:
+      - 3000:3000

+ 21 - 4
hubcmdui/server.js

@@ -26,8 +26,19 @@ const USERS_FILE = path.join(__dirname, 'users.json');
 async function readConfig() {
   try {
     const data = await fs.readFile(CONFIG_FILE, 'utf8');
+    // 确保 data 不为空或不完整
+    if (!data.trim()) {
+      console.warn('Config file is empty, returning default config');
+      return {
+        logo: '',
+        menuItems: [],
+        adImage: { url: '', link: '' }
+      };
+    }
+    console.log('Config read successfully');
     return JSON.parse(data);
   } catch (error) {
+    console.error('Failed to read config:', error);
     if (error.code === 'ENOENT') {
       return {
         logo: '',
@@ -41,7 +52,13 @@ async function readConfig() {
 
 // 写入配置
 async function writeConfig(config) {
-  await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
+  try {
+      await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
+      console.log('Config saved successfully');
+  } catch (error) {
+      console.error('Failed to save config:', error);
+      throw error;
+  }
 }
 
 // 读取用户
@@ -116,10 +133,10 @@ app.get('/api/config', async (req, res) => {
 // API 端点:保存配置
 app.post('/api/config', requireLogin, async (req, res) => {
   try {
-    await writeConfig(req.body);
-    res.json({ success: true });
+      await writeConfig(req.body);
+      res.json({ success: true });
   } catch (error) {
-    res.status(500).json({ error: 'Failed to save config' });
+      res.status(500).json({ error: 'Failed to save config' });
   }
 });
 

+ 76 - 69
hubcmdui/web/admin.html

@@ -162,6 +162,7 @@
 <body>
     <div class="container hidden" id="adminContainer">
         <h1 class="admin-title">Docker 镜像代理加速 - 管理面板</h1>
+        <p></h1>配置添加或修改后,点击【保存更改】保存配置</p>
         <form id="adminForm">
             <label for="logoUrl">Logo URL: (可选)</label>
             <input type="url" id="logoUrl" name="logoUrl">
@@ -241,34 +242,6 @@
             renderMenuItems();
         }
 
-        function renderMenuItems() {
-            const tbody = document.getElementById('menuTableBody');
-            tbody.innerHTML = '';
-            menuItems.forEach((item, index) => {
-                const row = `
-                    <tr data-index="${index}">
-                        <td><input type="text" class="menu-text" value="${item.text}" disabled></td>
-                        <td><input type="url" class="menu-link" value="${item.link || ''}" disabled></td>
-                        <td>
-                            <select class="menu-newtab" disabled>
-                                <option value="false" ${item.newTab ? '' : 'selected'}>否</option>
-                                <option value="true" ${item.newTab ? 'selected' : ''}>是</option>
-                            </select>
-                        </td>
-                        <td>
-                            <button type="button" class="action-btn edit-btn">编辑</button>
-                            <button type="button" class="action-btn delete-btn">删除</button>
-                            <span class="drag-handle" style="cursor: move;">☰</span>
-                        </td>
-                    </tr>
-                `;
-                tbody.innerHTML += row;
-            });
-            setupDragAndDrop();
-            setupEditButtons();
-            setupDeleteButtons();
-        }
-
         function setupEditButtons() {
             const editButtons = document.querySelectorAll('.edit-btn');
             editButtons.forEach((button, index) => {
@@ -283,16 +256,15 @@
                         linkInput.disabled = false;
                         newTabSelect.disabled = false;
                         button.textContent = '保存';
-                        editingIndex = row.getAttribute('data-index');
                     } else {
                         const text = textInput.value;
                         const link = linkInput.value;
                         const newTab = newTabSelect.value === 'true';
 
                         if (text) {
-                            menuItems[editingIndex] = { text, link, newTab };
+                            const rowIndex = row.getAttribute('data-index');
+                            menuItems[rowIndex] = { text, link, newTab };
                             renderMenuItems();
-                            editingIndex = -1;
                         } else {
                             alert('请填写菜单项文本');
                         }
@@ -300,6 +272,35 @@
                 });
             });
         }
+ 
+        function renderMenuItems() {
+            const tbody = document.getElementById('menuTableBody');
+            tbody.innerHTML = '';
+            menuItems.forEach((item, index) => {
+                const row = `
+                    <tr data-index="${index}">
+                        <td><input type="text" class="menu-text" value="${item.text}" disabled></td>
+                        <td><input type="url" class="menu-link" value="${item.link || ''}" disabled></td>
+                        <td>
+                            <select class="menu-newtab" disabled>
+                                <option value="false" ${item.newTab ? '' : 'selected'}>否</option>
+                                <option value="true" ${item.newTab ? 'selected' : ''}>是</option>
+                            </select>
+                        </td>
+                        <td>
+                            <button type="button" class="action-btn edit-btn">编辑</button>
+                            <button type="button" class="action-btn delete-btn">删除</button>
+                            <span class="drag-handle" style="cursor: move;">☰</span>
+                        </td>
+                    </tr>
+                `;
+                tbody.innerHTML += row;
+            });
+            setupDragAndDrop();
+            setupEditButtons();
+            setupDeleteButtons();
+        }
+        
 
         function setupDeleteButtons() {
             const deleteButtons = document.querySelectorAll('.delete-btn');
@@ -317,10 +318,10 @@
             const tbody = document.getElementById('menuTableBody');
             const newRow = `
                 <tr id="newMenuItemRow">
-                    <td><input type="text" id="newMenuItemText" placeholder="菜单项文本"></td>
-                    <td><input type="url" id="newMenuItemLink" placeholder="菜单项链接 (可选)"></td>
+                    <td><input type="text" class="menu-text" placeholder="菜单项文本"></td>
+                    <td><input type="url" class="menu-link" placeholder="菜单项链接 (可选)"></td>
                     <td>
-                        <select id="newMenuItemNewTab">
+                        <select class="menu-newtab">
                             <option value="false">否</option>
                             <option value="true">是</option>
                         </select>
@@ -334,13 +335,19 @@
             tbody.insertAdjacentHTML('beforeend', newRow);
         }
 
+
         function saveNewMenuItem() {
-            const text = document.getElementById('newMenuItemText').value;
-            const link = document.getElementById('newMenuItemLink').value;
-            const newTab = document.getElementById('newMenuItemNewTab').value === 'true';
-            
+            const newRow = document.getElementById('newMenuItemRow');
+            const textInput = newRow.querySelector('.menu-text');
+            const linkInput = newRow.querySelector('.menu-link');
+            const newTabSelect = newRow.querySelector('.menu-newtab');
+
+            const text = textInput.value;
+            const link = linkInput.value;
+            const newTab = newTabSelect.value === 'true';
+
             if (text) {
-                menuItems.push({ text, link, newTab });
+                menuItems.push({ text, link, newTab }); // 确保新菜单项被添加到 menuItems 数组中
                 renderMenuItems();
                 cancelNewMenuItem();
             } else {
@@ -468,7 +475,7 @@
             const config = {
                 logo: document.getElementById('logoUrl').value,
                 proxyDomain: document.getElementById('proxyDomain').value,
-                menuItems: getMenuItems(),
+                menuItems: menuItems,
                 adImages: adImages
             };
 
@@ -486,20 +493,20 @@
             } catch (error) {
                 alert('保存失败: ' + error.message);
             }
-        }
+        }      
 
         async function loadConfig() {
-            try {
-                const response = await fetch('/api/config');
-                const config = await response.json();
-                document.getElementById('logoUrl').value = config.logo || '';
-                document.getElementById('proxyDomain').value = config.proxyDomain || '';
-                setMenuItems(config.menuItems || []);
-                adImages = config.adImages || [];
-                renderAdItems();
-            } catch (error) {
-                console.error('加载配置失败:', error);
-            }
+        try {
+            const response = await fetch('/api/config');
+            const config = await response.json();
+            document.getElementById('logoUrl').value = config.logo || '';
+            document.getElementById('proxyDomain').value = config.proxyDomain || '';
+            setMenuItems(config.menuItems || []);
+            adImages = config.adImages || [];
+            renderAdItems();
+          } catch (error) {
+            console.error('加载配置失败:', error);
+          }
         }
 
         async function login() {
@@ -550,25 +557,25 @@
 
         // 页面加载时检查登录状态
         window.onload = async function() {
-            try {
-                const response = await fetch('/api/check-session');
-                if (response.ok) {
-                    isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';  // 读取登录状态
-                    if (isLoggedIn) {
-                        document.getElementById('loginModal').style.display = 'none';
-                        document.getElementById('adminContainer').classList.remove('hidden');
-                        loadConfig();
-                    } else {
-                        document.getElementById('loginModal').style.display = 'block';
-                    }
-                } else {
-                    localStorage.removeItem('isLoggedIn');  // 清除登录状态
-                    document.getElementById('loginModal').style.display = 'block';
-                }
-            } catch (error) {
-                localStorage.removeItem('isLoggedIn');  // 清除登录状态
+        try {
+            const response = await fetch('/api/check-session');
+            if (response.ok) {
+            isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
+            if (isLoggedIn) {
+                document.getElementById('loginModal').style.display = 'none';
+                document.getElementById('adminContainer').classList.remove('hidden');
+                loadConfig();
+            } else {
                 document.getElementById('loginModal').style.display = 'block';
             }
+            } else {
+            localStorage.removeItem('isLoggedIn');
+            document.getElementById('loginModal').style.display = 'block';
+            }
+          } catch (error) {
+            localStorage.removeItem('isLoggedIn');
+            document.getElementById('loginModal').style.display = 'block';
+          }
         };
 
         // 表单提交事件监听器

+ 28 - 13
hubcmdui/web/index.html

@@ -327,21 +327,36 @@
 
         // 复制命令到剪贴板
         function copyToClipboard(text, notificationId) {
-            navigator.clipboard.writeText(text).then(() => {
-                var notification = document.getElementById(notificationId);
-                notification.style.display = 'block';
-                notification.style.opacity = '1';
+            if (navigator.clipboard && navigator.clipboard.writeText) {
+                navigator.clipboard.writeText(text).then(() => {
+                    showNotification(notificationId);
+                }, (err) => {
+                    console.error('无法复制文本: ', err);
+                });
+            } else {
+                // 回退方案:使用临时的 <textarea> 元素
+                const textarea = document.createElement('textarea');
+                textarea.value = text;
+                document.body.appendChild(textarea);
+                textarea.select();
+                document.execCommand('copy');
+                document.body.removeChild(textarea);
+                showNotification(notificationId);
+            }
+        }
 
-                // 在2秒后隐藏提示
+        function showNotification(notificationId) {
+            var notification = document.getElementById(notificationId);
+            notification.style.display = 'block';
+            notification.style.opacity = '1';
+
+            // 在2秒后隐藏提示
+            setTimeout(function() {
+                notification.style.opacity = '0';
                 setTimeout(function() {
-                    notification.style.opacity = '0';
-                    setTimeout(function() {
-                        notification.style.display = 'none';
-                    }, 500);
-                }, 2000);
-            }, (err) => {
-                console.error('无法复制文本: ', err);
-            });
+                    notification.style.display = 'none';
+                }, 500);
+            }, 2000);
         }
 
         // 滚动到顶部