mirror of
https://github.com/dqzboy/Docker-Proxy.git
synced 2026-05-09 07:06:27 +02:00
feat: Add Docker Compose Deployment
This commit is contained in:
@@ -1,15 +1,11 @@
|
||||
# 使用官方的 Node.js 运行时镜像作为基础镜像
|
||||
FROM node:lts-alpine
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
# 复制项目文件到工作目录
|
||||
COPY hubcmdui/ .
|
||||
# 安装项目依赖
|
||||
RUN npm install
|
||||
|
||||
# 暴露应用程序的端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 运行应用程序
|
||||
CMD ["node", "server.js"]
|
||||
@@ -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
hubcmdui/docker-compose.yaml
Normal file
8
hubcmdui/docker-compose.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
## HubCMD UI
|
||||
hubcmd-ui:
|
||||
container_name: hubcmd-ui
|
||||
image: dqzboy/hubcmd-ui:latest
|
||||
restart: always
|
||||
ports:
|
||||
- 3000:3000
|
||||
@@ -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' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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,6 +242,37 @@
|
||||
renderMenuItems();
|
||||
}
|
||||
|
||||
function setupEditButtons() {
|
||||
const editButtons = document.querySelectorAll('.edit-btn');
|
||||
editButtons.forEach((button, index) => {
|
||||
button.addEventListener('click', () => {
|
||||
const row = button.closest('tr');
|
||||
const textInput = row.querySelector('.menu-text');
|
||||
const linkInput = row.querySelector('.menu-link');
|
||||
const newTabSelect = row.querySelector('.menu-newtab');
|
||||
|
||||
if (textInput.disabled) {
|
||||
textInput.disabled = false;
|
||||
linkInput.disabled = false;
|
||||
newTabSelect.disabled = false;
|
||||
button.textContent = '保存';
|
||||
} else {
|
||||
const text = textInput.value;
|
||||
const link = linkInput.value;
|
||||
const newTab = newTabSelect.value === 'true';
|
||||
|
||||
if (text) {
|
||||
const rowIndex = row.getAttribute('data-index');
|
||||
menuItems[rowIndex] = { text, link, newTab };
|
||||
renderMenuItems();
|
||||
} else {
|
||||
alert('请填写菜单项文本');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderMenuItems() {
|
||||
const tbody = document.getElementById('menuTableBody');
|
||||
tbody.innerHTML = '';
|
||||
@@ -268,38 +300,7 @@
|
||||
setupEditButtons();
|
||||
setupDeleteButtons();
|
||||
}
|
||||
|
||||
function setupEditButtons() {
|
||||
const editButtons = document.querySelectorAll('.edit-btn');
|
||||
editButtons.forEach((button, index) => {
|
||||
button.addEventListener('click', () => {
|
||||
const row = button.closest('tr');
|
||||
const textInput = row.querySelector('.menu-text');
|
||||
const linkInput = row.querySelector('.menu-link');
|
||||
const newTabSelect = row.querySelector('.menu-newtab');
|
||||
|
||||
if (textInput.disabled) {
|
||||
textInput.disabled = false;
|
||||
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 };
|
||||
renderMenuItems();
|
||||
editingIndex = -1;
|
||||
} else {
|
||||
alert('请填写菜单项文本');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
// 表单提交事件监听器
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 滚动到顶部
|
||||
|
||||
Reference in New Issue
Block a user