feat: Add Docker Compose Deployment

This commit is contained in:
dqzboy
2024-07-24 18:45:45 +08:00
parent 4f0ac23483
commit 103b6c36d5
6 changed files with 151 additions and 91 deletions

View File

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

View File

@@ -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": []
}

View File

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

View File

@@ -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' });
}
});

View File

@@ -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';
}
};
// 表单提交事件监听器

View File

@@ -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);
}
// 滚动到顶部