feat: Add instructions for obtaining proxy acceleration to the UI panel.

This commit is contained in:
dqzboy
2024-07-23 20:21:59 +08:00
parent 260ca63979
commit dd3b367bcd
9 changed files with 759 additions and 3 deletions

15
Dockerfile Normal file
View File

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

BIN
hubcmdui/.DS_Store vendored Normal file

Binary file not shown.

120
hubcmdui/README.md Normal file
View File

@@ -0,0 +1,120 @@
<p align="right">
<strong>中文</strong> | <a href="./README.en.md">English</a>
</p>
<div style="text-align: center">
<p align="center">
<img src="https://github.com/dqzboy/Docker-Proxy/assets/42825450/c187d66f-152e-4172-8268-e54bd77d48bb" width="230px" height="200px">
<br>
<i>Docker-Proxy 镜像代理加速快速获取命令UI面板.</i>
</p>
</div>
<div align="center">
[![Auth](https://img.shields.io/badge/Auth-dqzboy-ff69b4)](https://github.com/dqzboy)
[![GitHub contributors](https://img.shields.io/github/contributors/dqzboy/Docker-Proxy)](https://github.com/dqzboy/Docker-Proxy/graphs/contributors)
[![GitHub Issues](https://img.shields.io/github/issues/dqzboy/Docker-Proxy.svg)](https://github.com/dqzboy/Docker-Proxy/issues)
[![GitHub Pull Requests](https://img.shields.io/github/stars/dqzboy/Docker-Proxy)](https://github.com/dqzboy/Docker-Proxy)
[![HitCount](https://views.whatilearened.today/views/github/dqzboy/Docker-Proxy.svg)](https://github.com/dqzboy/Docker-Proxy)
[![GitHub license](https://img.shields.io/github/license/dqzboy/Docker-Proxy)](https://github.com/dqzboy/Docker-Proxy/blob/main/LICENSE)
📢 <a href="https://t.me/+ghs_XDp1vwxkMGU9" style="font-size: 15px;">Docker Proxy-交流群</a>
</div>
---
## 📝 本地运行
#### 1. 克隆项目
```bash
git clone git@github.com:dqzboy/Docker-Proxy.git
```
#### 2. 安装依赖
```bash
cd Docker-Proxy/hubcmdui
npm install
```
#### 3. 启动服务
```bash
node server.js
```
## 📦 Docker运行
- 如果不自行构建Docker镜像可直接跳转第三步拉取镜像运行
#### 1. 克隆项目(可选)
```bash
git clone git@github.com:dqzboy/Docker-Proxy.git
```
#### 2. 构建镜像(可选)
```bash
cd Docker-Proxy
docker build -t hubcmd-ui .
```
#### 3. 运行容器
```bash
# 拉取镜像!如果你手动构建了镜像此步骤跳过
docker pull dqzboy/hubcmd-ui:latest
docker run -d -p 3000:3000 --name hubcmdui-server hubcmd-ui
```
---
## UI
- 默认监听3000端口
> 浏览器输入 `服务器地址:3000` 访问前端
<table>
<tr>
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/142ac19b-933f-46e8-85f9-5cb60a14c5cd"?raw=true"></td>
</tr>
</table>
> 浏览器输入 `服务器地址/admin:3000` 访问后端页面
<table>
<tr>
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/d2f76296-e329-4941-9292-8d3d43e2bea4"?raw=true"></td>
</tr>
</table>
<table>
<tr>
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c3551c10-a2b4-431b-87b9-f5d408162e61"?raw=true"></td>
</tr>
</table>
---
## 🫶 赞助
如果你觉得这个项目对你有帮助请给我点个Star。并且情况允许的话可以给我一点点支持总之非常感谢支持😊
> 项目作者自建公益服务:[服务地址查看](https://uk.dqzboy.xyz/)
<table>
<tr>
<td width="50%" align="center"><b> Alipay </b></td>
<td width="50%" align="center"><b> WeChat Pay </b></td>
</tr>
<tr>
<td width="50%" align="center"><img src="https://github.com/dqzboy/Deploy_K8sCluster/assets/42825450/223fd099-9433-468b-b490-f9807bdd2035?raw=true"></td>
<td width="50%" align="center"><img src="https://github.com/dqzboy/Deploy_K8sCluster/assets/42825450/9404460f-ea1b-446c-a0ae-6da96eb459e3?raw=true"></td>
</tr>
</table>
---
## 😺 其他
开源不易,若你参考此项目或基于此项目修改可否麻烦在你的项目文档中标识此项目?谢谢你!

0
hubcmdui/config.json Normal file
View File

6
hubcmdui/package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"dependencies": {
"express": "^4.19.2",
"express-session": "^1.18.0"
}
}

140
hubcmdui/server.js Normal file
View File

@@ -0,0 +1,140 @@
// server.js
const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const bodyParser = require('body-parser');
const session = require('express-session');
const app = express();
app.use(express.json());
app.use(express.static('web'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({
secret: 'OhTq3faqSKoxbV%NJV',
resave: false,
saveUninitialized: true,
cookie: { secure: false } // 设置为true如果使用HTTPS
}));
app.get('/admin', (req, res) => {
res.sendFile(path.join(__dirname, 'web', 'admin.html'));
});
const CONFIG_FILE = path.join(__dirname, 'config.json');
const USERS_FILE = path.join(__dirname, 'users.json');
// 读取配置
async function readConfig() {
try {
const data = await fs.readFile(CONFIG_FILE, 'utf8');
return JSON.parse(data);
} catch (error) {
if (error.code === 'ENOENT') {
return {
logo: '',
menuItems: [],
adImage: { url: '', link: '' }
};
}
throw error;
}
}
// 写入配置
async function writeConfig(config) {
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
}
// 读取用户
async function readUsers() {
try {
const data = await fs.readFile(USERS_FILE, 'utf8');
return JSON.parse(data);
} catch (error) {
if (error.code === 'ENOENT') {
return {
users: [{ username: 'root', password: 'admin' }]
};
}
throw error;
}
}
// 写入用户
async function writeUsers(users) {
await fs.writeFile(USERS_FILE, JSON.stringify(users, null, 2), 'utf8');
}
// 登录验证
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
const users = await readUsers();
const user = users.users.find(u => u.username === username && u.password === password);
if (user) {
req.session.user = user;
res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
// 修改密码
app.post('/api/change-password', async (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Not logged in' });
}
const { currentPassword, newPassword } = req.body;
const users = await readUsers();
const user = users.users.find(u => u.username === req.session.user.username);
if (user && user.password === currentPassword) {
user.password = newPassword;
await writeUsers(users);
res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid current password' });
}
});
// 需要登录验证的中间件
function requireLogin(req, res, next) {
if (req.session.user) {
next();
} else {
res.status(401).json({ error: 'Not logged in' });
}
}
// API 端点:获取配置
app.get('/api/config', requireLogin, async (req, res) => {
try {
const config = await readConfig();
res.json(config);
} catch (error) {
res.status(500).json({ error: 'Failed to read config' });
}
});
// API 端点:保存配置
app.post('/api/config', requireLogin, async (req, res) => {
try {
await writeConfig(req.body);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: 'Failed to save config' });
}
});
// API 端点:检查会话状态
app.get('/api/check-session', (req, res) => {
if (req.session.user) {
res.json({ success: true });
} else {
res.status(401).json({ error: 'Not logged in' });
}
});
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

8
hubcmdui/users.json Normal file
View File

@@ -0,0 +1,8 @@
{
"users": [
{
"username": "root",
"password": "admin"
}
]
}

427
hubcmdui/web/admin.html Normal file
View File

@@ -0,0 +1,427 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Docker 镜像代理加速 - 管理面板</title>
<link rel="icon" href="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" type="image/png">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Lato', 'Helvetica', 'Arial', sans-serif;
line-height: 1.6;
color: #24292e;
margin: 0;
padding: 20px;
background-color: #f6f8fa;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1, h2 {
border-bottom: 2px solid #eaecef;
padding-bottom: 0.5em;
margin-bottom: 1em;
color: #0366d6;
}
label {
display: block;
margin-top: 20px;
border-bottom: none;
padding-bottom: 0;
color: #24292e;
font-size: 20px;
}
input[type="text"], input[type="url"], input[type="password"], select {
width: 100%;
padding: 8px;
margin-top: 5px;
border: 1px solid #d1d5da;
border-radius: 4px;
box-sizing: border-box;
font-family: inherit;
font-size: inherit;
}
button {
background-color: #2ea44f;
color: white;
border: none;
padding: 10px 20px;
margin-top: 20px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: inherit;
}
button:hover {
background-color: #2c974b;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #d1d5da;
padding: 8px;
text-align: left;
}
th {
background-color: #f6f8fa;
font-weight: normal;
}
.action-btn {
background-color: #0366d6;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
margin-right: 5px;
font-family: inherit;
font-size: inherit;
}
.action-btn:hover {
background-color: #0256b9;
}
.add-btn {
background-color: #2ea44f;
color: white;
border: none;
padding: 10px 20px;
margin-top: 10px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: inherit;
}
.add-btn:hover {
background-color: #2c974b;
}
#menuPreview {
margin-top: 20px;
padding: 10px;
background-color: #f6f8fa;
border: 1px solid #d1d5da;
border-radius: 4px;
}
#menuPreview a {
margin-right: 15px;
color: #0366d6;
text-decoration: none;
}
#menuPreview a:hover {
text-decoration: underline;
}
.hidden {
display: none;
}
.login-modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}
.login-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 30%;
border-radius: 8px;
}
.user-management {
position: absolute;
top: 20px;
right: 20px;
cursor: pointer;
}
.user-management i {
font-size: 24px;
color: #0366d6;
}
.menu-label {
font-size: 20px;
color: #24292e;
}
.admin-title {
font-size: 24px;
color: #0366d6;
}
</style>
</head>
<body>
<div class="container hidden" id="adminContainer">
<h1 class="admin-title">Docker 镜像代理加速 - 管理面板</h1>
<form id="adminForm">
<label for="logoUrl">Logo URL: (可选)</label>
<input type="url" id="logoUrl" name="logoUrl">
<label for="proxyDomain">Docker镜像代理地址: (必填)</label>
<input type="text" id="proxyDomain" name="proxyDomain" required>
<h2 class="menu-label">菜单项管理</h2>
<table id="menuTable">
<thead>
<tr>
<th>文本</th>
<th>链接 (可选)</th>
<th>新标签页打开</th>
<th>操作</th>
</tr>
</thead>
<tbody id="menuTableBody">
<!-- 菜单项将在这里动态添加 -->
</tbody>
<tfoot>
<tr id="newMenuItemRow" class="hidden">
<td><input type="text" id="newMenuItemText" placeholder="菜单项文本"></td>
<td><input type="url" id="newMenuItemLink" placeholder="菜单项链接 (可选)"></td>
<td>
<select id="newMenuItemNewTab">
<option value="false"></option>
<option value="true"></option>
</select>
</td>
<td>
<button type="button" class="action-btn" onclick="saveNewMenuItem()">保存</button>
<button type="button" class="action-btn" onclick="cancelNewMenuItem()">取消</button>
</td>
</tr>
</tfoot>
</table>
<button type="button" class="add-btn" onclick="showNewMenuItemRow()">添加菜单项</button>
<h2 class="menu-label">菜单预览</h2>
<div id="menuPreview">
<!-- 菜单预览将在这里显示 -->
</div>
<label for="adImageUrl">广告图片 URL: (可选)</label>
<input type="url" id="adImageUrl" name="adImageUrl">
<!-- 添加修改密码的选项 -->
<h2 class="menu-label">修改密码</h2>
<label for="currentPassword">当前密码</label>
<input type="password" id="currentPassword" name="currentPassword" required>
<label for="newPassword">新密码</label>
<input type="password" id="newPassword" name="newPassword" required>
<button type="button" onclick="changePassword()">修改密码</button>
<button type="submit">保存更改</button>
</form>
</div>
<div class="login-modal" id="loginModal">
<div class="login-content">
<h2>登录</h2>
<label for="username">用户名</label>
<input type="text" id="username" name="username" required>
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
<button type="button" onclick="login()">登录</button>
</div>
</div>
<script>
let menuItems = [];
let isLoggedIn = false;
function getMenuItems() {
return menuItems;
}
function setMenuItems(items) {
menuItems = items;
renderMenuItems();
updateMenuPreview();
}
function renderMenuItems() {
const tbody = document.getElementById('menuTableBody');
tbody.innerHTML = '';
menuItems.forEach((item, index) => {
const row = `
<tr>
<td>${item.text}</td>
<td>${item.link || ''}</td>
<td>${item.newTab ? '是' : '否'}</td>
<td>
<button type="button" class="action-btn" onclick="editMenuItem(${index})">编辑</button>
<button type="button" class="action-btn" onclick="deleteMenuItem(${index})">删除</button>
</td>
</tr>
`;
tbody.innerHTML += row;
});
}
function updateMenuPreview() {
const preview = document.getElementById('menuPreview');
preview.innerHTML = menuItems.map(item =>
`<a href="${item.link || '#'}" ${item.newTab ? 'target="_blank"' : ''}>${item.text}</a>`
).join(' ');
}
function showNewMenuItemRow() {
document.getElementById('newMenuItemRow').classList.remove('hidden');
}
function saveNewMenuItem() {
const text = document.getElementById('newMenuItemText').value;
const link = document.getElementById('newMenuItemLink').value;
const newTab = document.getElementById('newMenuItemNewTab').value === 'true';
if (text) {
menuItems.push({ text, link, newTab });
renderMenuItems();
updateMenuPreview();
cancelNewMenuItem();
} else {
alert('请填写菜单项文本');
}
}
function cancelNewMenuItem() {
document.getElementById('newMenuItemRow').classList.add('hidden');
document.getElementById('newMenuItemText').value = '';
document.getElementById('newMenuItemLink').value = '';
document.getElementById('newMenuItemNewTab').value = 'false';
}
function editMenuItem(index) {
const item = menuItems[index];
document.getElementById('newMenuItemText').value = item.text;
document.getElementById('newMenuItemLink').value = item.link;
document.getElementById('newMenuItemNewTab').value = item.newTab.toString();
showNewMenuItemRow();
menuItems.splice(index, 1);
renderMenuItems();
updateMenuPreview();
}
function deleteMenuItem(index) {
menuItems.splice(index, 1);
renderMenuItems();
updateMenuPreview();
}
async function saveConfig() {
const config = {
logo: document.getElementById('logoUrl').value,
proxyDomain: document.getElementById('proxyDomain').value,
menuItems: getMenuItems(),
adImage: document.getElementById('adImageUrl').value
};
try {
const response = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (response.ok) {
alert('配置已保存');
} else {
throw new Error('保存失败');
}
} 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 || []);
document.getElementById('adImageUrl').value = config.adImage || '';
} catch (error) {
console.error('加载配置失败:', error);
}
}
async function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (response.ok) {
isLoggedIn = true;
localStorage.setItem('isLoggedIn', 'true'); // 存储登录状态
document.getElementById('loginModal').style.display = 'none';
document.getElementById('adminContainer').classList.remove('hidden');
loadConfig();
} else {
alert('登录失败');
}
} catch (error) {
alert('登录失败: ' + error.message);
}
}
async function changePassword() {
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
try {
const response = await fetch('/api/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ currentPassword, newPassword })
});
if (response.ok) {
alert('密码已修改');
} else {
alert('修改密码失败');
}
} catch (error) {
alert('修改密码失败: ' + error.message);
}
}
// 页面加载时检查登录状态
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'); // 清除登录状态
document.getElementById('loginModal').style.display = 'block';
}
};
// 表单提交事件监听器
document.getElementById('adminForm').addEventListener('submit', async function(e) {
e.preventDefault();
await saveConfig();
});
</script>
</body>
</html>

View File

@@ -247,7 +247,7 @@
<!-- 广告容器 -->
<div class="ad-container" id="adContainer">
<a href="https://example.com" target="_blank">
<img src="https://github.com/user-attachments/assets/595f645a-e4da-49fd-bd67-db93566a6152?text=Ad+Banner" alt="广告图片">
<img src="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/guanggao.png?text=Ad+Banner" alt="广告图片">
</a>
</div>
<!-- 结果容器 -->
@@ -268,6 +268,8 @@
// 设置当前年份
document.getElementById('currentYear').textContent = new Date().getFullYear();
let proxyDomain = 'your_domain'; // 默认代理加速地址
// 生成加速命令
function generateCommands() {
const imageInput = document.getElementById('imageInput').value.trim();
@@ -275,8 +277,6 @@
alert('请输入 Docker 镜像');
return;
}
// docker镜像加速地址请把下面your_domain改为你实际的加速地址
const proxyDomain = 'your_domain'; //例如hub.baidu.com
let [imageName, tag] = imageInput.split(':');
tag = tag || 'latest';
@@ -333,6 +333,46 @@
behavior: 'smooth'
});
}
// 获取并加载配置
async function loadConfig() {
try {
const response = await fetch('/api/config');
const config = await response.json();
if (config.logo) {
document.querySelector('.logo').src = config.logo;
}
if (config.menuItems && Array.isArray(config.menuItems)) {
const navMenu = document.querySelector('.nav-menu');
navMenu.innerHTML = ''; // 清空菜单
config.menuItems.forEach(item => {
const a = document.createElement('a');
a.href = item.link;
a.textContent = item.text;
if (item.newTab) {
a.target = '_blank';
}
navMenu.appendChild(a);
});
}
if (config.adImage && config.adImage.url) {
const adContainer = document.getElementById('adContainer');
const adLink = adContainer.querySelector('a');
const adImage = adContainer.querySelector('img');
adLink.href = config.adImage.link;
adImage.src = config.adImage.url;
adImage.alt = config.adImage.alt || '广告图片';
adContainer.style.display = 'flex';
}
if (config.proxyDomain) {
proxyDomain = config.proxyDomain;
}
} catch (error) {
console.error('加载配置失败:', error);
}
}
loadConfig();
</script>
</body>
</html>