Files
hubproxy/src/public/skopeo.html
2025-06-12 15:29:51 +08:00

717 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Docker镜像批量下载工具,docker镜像打包下载">
<meta name="keywords" content="Docker,镜像下载,skopeo,docker镜像打包">
<meta name="color-scheme" content="dark light">
<title>Docker镜像批量下载</title>
<link rel="icon" href="./favicon.ico">
<style>
/* 使用首页完全相同的颜色系统 */
:root {
--background: #ffffff;
--foreground: #0f172a;
--card: #ffffff;
--card-foreground: #0f172a;
--primary: #2563eb;
--primary-foreground: #f8fafc;
--secondary: #f1f5f9;
--secondary-foreground: #0f172a;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--accent: #f1f5f9;
--accent-foreground: #0f172a;
--border: #e2e8f0;
--input: #ffffff;
--ring: #2563eb;
--radius: 0.5rem;
}
.dark {
--background: #0f172a;
--foreground: #f8fafc;
--card: #1e293b;
--card-foreground: #f8fafc;
--primary: #3b82f6;
--primary-foreground: #f8fafc;
--secondary: #1e293b;
--secondary-foreground: #f8fafc;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--accent: #1e293b;
--accent-foreground: #f8fafc;
--border: #334155;
--input: #1e293b;
--ring: #3b82f6;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0f172a;
--foreground: #f8fafc;
--card: #1e293b;
--card-foreground: #f8fafc;
--primary: #3b82f6;
--primary-foreground: #f8fafc;
--secondary: #1e293b;
--secondary-foreground: #f8fafc;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--accent: #1e293b;
--accent-foreground: #f8fafc;
--border: #334155;
--input: #1e293b;
--ring: #3b82f6;
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background-color: var(--background);
color: var(--foreground);
line-height: 1.5;
min-height: 100vh;
display: flex;
flex-direction: column;
transition: background-color 0.3s, color 0.3s;
}
/* 导航栏样式 - 与首页完全一致,使用!important确保优先级 */
.navbar {
position: sticky !important;
top: 0 !important;
z-index: 50 !important;
width: 100% !important;
border-bottom: 1px solid var(--border) !important;
background-color: var(--background) !important;
backdrop-filter: blur(8px) !important;
background-color: rgba(255, 255, 255, 0.95) !important;
padding: 0 !important;
margin: 0 !important;
}
.dark .navbar {
background-color: rgba(15, 23, 42, 0.95) !important;
}
.navbar-container {
max-width: 1200px !important;
margin: 0 auto !important;
padding: 0 1rem !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
height: 4rem !important;
}
.logo {
display: flex !important;
align-items: center !important;
gap: 0.5rem !important;
text-decoration: none !important;
color: var(--foreground) !important;
font-weight: 600 !important;
font-size: 1.125rem !important;
}
.logo-icon {
width: 2rem !important;
height: 2rem !important;
border-radius: 0.5rem !important;
background: linear-gradient(135deg, var(--primary), #3b82f6) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
color: white !important;
}
.nav-links {
display: flex !important;
align-items: center !important;
gap: 0.5rem !important;
}
.nav-link {
padding: 0.5rem 1rem !important;
border-radius: var(--radius) !important;
text-decoration: none !important;
color: var(--muted-foreground) !important;
transition: all 0.2s !important;
font-weight: 500 !important;
}
.nav-link:hover,
.nav-link.active {
color: var(--foreground) !important;
background-color: var(--muted) !important;
}
.theme-toggle {
padding: 0.5rem !important;
border: none !important;
border-radius: var(--radius) !important;
background-color: transparent !important;
color: var(--muted-foreground) !important;
cursor: pointer !important;
transition: all 0.2s !important;
}
.theme-toggle:hover {
background-color: var(--muted) !important;
color: var(--foreground) !important;
}
/* 主要内容区域 */
.main {
flex: 1;
padding: 2rem 1rem;
}
*::-webkit-scrollbar {
height: 10px;
margin-top: 0px;
}
*::-webkit-scrollbar-track {
background-color: var(--muted);
}
*::-webkit-scrollbar-thumb {
background: var(--primary);
border-radius: 10px;
}
.container {
max-width: 80%;
text-align: center;
min-height: 65%;
line-height: 1.25;
margin: 2rem auto 0; /* 保持原有布局但使用更标准的边距 */
}
h1 {
color: var(--foreground);
font-weight: bold;
margin-bottom: 30px;
}
.rounded-button {
border-radius: 6px;
transition: background-color 0.3s, transform 0.2s;
padding: 10px 30px;
background-color: var(--primary);
color: var(--primary-foreground);
border: none;
margin-bottom: 3%;
}
.rounded-button:hover {
background-color: #1d4ed8;
transform: scale(1.05);
}
footer {
position: fixed;
bottom: 20px;
left: 0;
right: 0;
text-align: center;
padding: 10px;
line-height: 1.25;
margin-top: 20px;
}
pre {
background: var(--muted);
color: var(--primary);
padding: 15px 20px 15px 20px;
margin: 0px 0;
border-radius: 0.5rem;
overflow-x: auto;
position: relative;
border: 1px solid var(--border);
}
pre::before {
content: " ";
display: block;
position: absolute;
top: 6px;
left: 6px;
width: 10px;
height: 10px;
background: #dc3545;
border-radius: 50%;
box-shadow: 20px 0 0 #ffc107, 40px 0 0 #28a745;
}
code {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.9em;
margin-bottom: 0px;
}
@media (max-width: 768px) {
.container {
max-width: 100%;
font-size: 0.8rem;
}
.nav-links {
display: none;
}
}
@media (min-width: 768px) {
.container {
max-width: 65%;
font-size: 1rem;
}
h1 {
margin-bottom: 30px;
}
}
.form-group {
margin-bottom: 3%;
}
/* 基础样式定义替代Bootstrap */
.btn {
display: inline-block;
padding: 0.375rem 0.75rem;
margin-bottom: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
border: 1px solid transparent;
border-radius: 0.375rem;
text-decoration: none;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control {
display: block;
width: 100%;
background-color: var(--input);
color: var(--foreground);
border: 1px solid var(--border);
border-radius: 0.375rem;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
background-color: var(--input);
color: var(--foreground);
border-color: var(--ring);
box-shadow: 0 0 0 0.2rem rgba(37, 99, 235, 0.25);
outline: 0;
}
#toast {
position: fixed;
top: 10%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--primary);
color: var(--primary-foreground);
padding: 15px 20px;
border-radius: 10px;
font-size: 90%;
z-index: 1000;
}
.back-button {
position: fixed;
top: 20px;
left: 20px;
padding: 2px 8px;
background-color: var(--muted);
border: 1px solid var(--border);
color: var(--foreground);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
text-decoration: none;
}
.back-button:hover {
background-color: var(--primary);
color: var(--primary-foreground);
transform: scale(1.05);
text-decoration: none;
}
.progress-container {
margin-top: 20px;
display: none;
}
.total-progress-text {
font-size: 16px;
font-weight: bold;
margin-bottom: 20px;
padding: 10px;
background-color: var(--input);
border-radius: 10px;
display: inline-block;
}
.image-progress {
margin-top: 15px;
margin-bottom: 5px;
}
.image-progress-item {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.image-progress-name {
flex: 0 0 200px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.image-progress-bar-container {
flex-grow: 1;
height: 15px;
background-color: #ddd;
border-radius: 5px;
margin: 0 10px;
}
.image-progress-bar {
height: 100%;
background-color: #39c5bb;
border-radius: 5px;
width: 0%;
}
.image-progress-text {
flex: 0 0 50px;
text-align: right;
}
.download-button {
display: none;
margin-top: 20px;
padding: 10px 20px;
background-color: var(--primary);
color: var(--primary-foreground);
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.download-button:hover {
background-color: #1d4ed8;
transform: scale(1.05);
}
textarea.form-control {
min-height: 150px;
resize: vertical;
}
.info-text {
font-size: 0.9rem;
color: var(--foreground);
opacity: 0.8;
margin-bottom: 15px;
text-align: left;
}
</style>
</head>
<body>
<!-- 现代化导航栏 -->
<nav class="navbar">
<div class="navbar-container">
<a href="/" class="logo">
<div class="logo-icon">
</div>
加速服务
</a>
<div class="nav-links">
<a href="/" class="nav-link">🚀 GitHub加速</a>
<a href="/skopeo.html" class="nav-link active">🐳 镜像下载</a>
<a href="/search.html" class="nav-link">🔍 镜像搜索</a>
<a href="https://gitee.com/if-the-wind/github-hosts/raw/main/hosts" target="_blank" class="nav-link">📄 Hosts</a>
<button class="theme-toggle" id="themeToggle">
🌙
</button>
</div>
</div>
</nav>
<main class="main">
<div class="container">
<h1>Docker离线镜像包下载</h1>
<div class="form-group">
<div class="info-text">每行输入一个镜像跟docker pull的格式一样多个镜像会自动打包到一起为zip包单个镜像为tar包。导入镜像后需要手动为镜像添加名称和标签例如docker tag 1856948a5aa7 镜像名称:标签</div>
<textarea class="form-control" id="imageInput" placeholder="例如:&#10;nginx&#10;stilleshan/frpc&#10;stilleshan/frpc:0.62.1"></textarea>
</div>
<div class="form-group">
<div class="info-text">镜像架构,默认为 amd64</div>
<input type="text" class="form-control" id="platformInput" placeholder="输入架构例如amd64, arm64等" value="amd64">
</div>
<button class="btn rounded-button" id="downloadButton">开始下载</button>
<div class="progress-container" id="progressContainer">
<div class="total-progress-text" id="totalProgressText">0/0 - 0%</div>
<div class="image-progress" id="imageProgressList">
<!-- Image progress items will be added here -->
</div>
<button class="download-button" id="getFileButton">下载文件</button>
</div>
</div>
<div id="toast" style="display:none;"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const imageInput = document.getElementById('imageInput');
const platformInput = document.getElementById('platformInput');
const downloadButton = document.getElementById('downloadButton');
const progressContainer = document.getElementById('progressContainer');
const totalProgressText = document.getElementById('totalProgressText');
const imageProgressList = document.getElementById('imageProgressList');
const getFileButton = document.getElementById('getFileButton');
let images = [];
let currentTaskId = null;
let websocket = null;
function parseImageList() {
const text = imageInput.value.trim();
if (!text) {
return [];
}
return text.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
}
function startDownload() {
images = parseImageList();
if (images.length === 0) {
showToast('请至少输入一个镜像');
return;
}
const platform = platformInput.value.trim() || 'amd64';
const requestData = {
images: images,
platform: platform
};
fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
if (data.taskId) {
currentTaskId = data.taskId;
showProgressUI();
connectWebSocket(currentTaskId);
const totalCount = data.totalCount || images.length;
totalProgressText.textContent = `0/${totalCount} - 0%`;
} else {
showToast('下载任务创建失败');
}
})
.catch(error => {
showToast('请求失败: ' + error.message);
});
}
function showProgressUI() {
progressContainer.style.display = 'block';
downloadButton.style.display = 'none';
imageInput.disabled = true;
platformInput.disabled = true;
const totalCount = images.length;
totalProgressText.textContent = `0/${totalCount} - 0%`;
imageProgressList.innerHTML = '';
images.forEach(image => {
addImageProgressItem(image, 0);
});
}
function addImageProgressItem(image, progress) {
const itemDiv = document.createElement('div');
itemDiv.className = 'image-progress-item';
itemDiv.id = `progress-${image.replace(/[\/\.:]/g, '_')}`;
const nameDiv = document.createElement('div');
nameDiv.className = 'image-progress-name';
nameDiv.title = image;
nameDiv.textContent = image;
const barContainerDiv = document.createElement('div');
barContainerDiv.className = 'image-progress-bar-container';
const barDiv = document.createElement('div');
barDiv.className = 'image-progress-bar';
barDiv.style.width = `${progress}%`;
const textDiv = document.createElement('div');
textDiv.className = 'image-progress-text';
textDiv.textContent = `${Math.round(progress)}%`;
barContainerDiv.appendChild(barDiv);
itemDiv.appendChild(nameDiv);
itemDiv.appendChild(barContainerDiv);
itemDiv.appendChild(textDiv);
imageProgressList.appendChild(itemDiv);
}
function updateImageProgress(image, progress, status) {
const itemId = `progress-${image.replace(/[\/\.:]/g, '_')}`;
const item = document.getElementById(itemId);
if (item) {
const bar = item.querySelector('.image-progress-bar');
const text = item.querySelector('.image-progress-text');
bar.style.width = `${progress}%`;
text.textContent = `${Math.round(progress)}%`;
if (status === 'failed') {
bar.style.backgroundColor = '#bd3c35';
text.textContent = '失败';
} else if (status === 'completed') {
bar.style.backgroundColor = '#4CAF50';
text.textContent = '完成';
}
}
}
function connectWebSocket(taskId) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/${taskId}`;
websocket = new WebSocket(wsUrl);
websocket.onopen = function() {
console.log('ws');
};
websocket.onmessage = function(event) {
const data = JSON.parse(event.data);
updateProgress(data);
};
websocket.onerror = function(error) {
console.error('WebSocket错误:', error);
showToast('WebSocket连接错误');
};
websocket.onclose = function() {
console.log('WebSocket连接已关闭');
};
}
function updateProgress(data) {
const progressPercent = data.totalCount > 0 ? (data.completedCount / data.totalCount) * 100 : 0;
totalProgressText.textContent = `${data.completedCount}/${data.totalCount} - ${Math.round(progressPercent)}%`;
data.images.forEach(imgData => {
updateImageProgress(imgData.image, imgData.progress, imgData.status);
});
if (data.status === 'completed') {
getFileButton.style.display = 'inline-block';
if (websocket) {
websocket.close();
}
}
}
function downloadFile() {
if (!currentTaskId) {
showToast('没有可下载的文件');
return;
}
window.location.href = `/api/files/${currentTaskId}_file`;
}
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.display = 'block';
setTimeout(() => {
toast.style.display = 'none';
}, 3000);
}
downloadButton.addEventListener('click', startDownload);
getFileButton.addEventListener('click', downloadFile);
// 主题切换功能
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
html.classList.add('dark');
themeToggle.textContent = '☀️';
}
themeToggle.addEventListener('click', () => {
html.classList.toggle('dark');
const isDark = html.classList.contains('dark');
themeToggle.textContent = isDark ? '☀️' : '🌙';
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
});
</script>
</main> <!-- 关闭 main -->
</body>
</html>