mirror of
https://github.com/sky22333/hubproxy.git
synced 2026-08-05 03:24:57 +08:00
1061 lines
34 KiB
HTML
1061 lines
34 KiB
HTML
<!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;
|
||
--success: #059669;
|
||
--warning: #d97706;
|
||
--error: #dc2626;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 现代化导航栏 */
|
||
.navbar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 50;
|
||
width: 100%;
|
||
border-bottom: 1px solid var(--border);
|
||
background-color: var(--background);
|
||
backdrop-filter: blur(8px);
|
||
background-color: rgba(255, 255, 255, 0.95);
|
||
}
|
||
|
||
.dark .navbar {
|
||
background-color: rgba(15, 23, 42, 0.95);
|
||
}
|
||
|
||
.navbar-container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 0 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 4rem;
|
||
}
|
||
|
||
.logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
text-decoration: none;
|
||
color: var(--foreground);
|
||
font-weight: 600;
|
||
font-size: 1.125rem;
|
||
}
|
||
|
||
.logo-icon {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
border-radius: 0.5rem;
|
||
background: linear-gradient(135deg, var(--primary), #3b82f6);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
}
|
||
|
||
.nav-links {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.nav-link {
|
||
padding: 0.5rem 1rem;
|
||
border-radius: var(--radius);
|
||
text-decoration: none;
|
||
color: var(--muted-foreground);
|
||
transition: all 0.2s;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.nav-link:hover,
|
||
.nav-link.active {
|
||
color: var(--foreground);
|
||
background-color: var(--muted);
|
||
}
|
||
|
||
.theme-toggle {
|
||
padding: 0.5rem;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
background-color: transparent;
|
||
color: var(--muted-foreground);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.theme-toggle:hover {
|
||
background-color: var(--muted);
|
||
color: var(--foreground);
|
||
}
|
||
|
||
/* 主要内容区域 */
|
||
.main {
|
||
flex: 1;
|
||
padding: 2rem 1rem;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.hero {
|
||
text-align: center;
|
||
margin-bottom: 3rem;
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
animation: fadeInUp 0.6s ease-out forwards;
|
||
}
|
||
|
||
.hero-title {
|
||
font-size: 2.5rem;
|
||
font-weight: 700;
|
||
margin-bottom: 1rem;
|
||
background: linear-gradient(135deg, var(--primary), #3b82f6);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
.hero-subtitle {
|
||
font-size: 1.125rem;
|
||
color: var(--muted-foreground);
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* 现代化卡片设计 */
|
||
.card {
|
||
background-color: var(--card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 0.75rem;
|
||
padding: 1.5rem;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
margin-bottom: 2rem;
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
animation: fadeInUp 0.6s ease-out 0.2s forwards;
|
||
}
|
||
|
||
.card-header {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 1.25rem;
|
||
font-weight: 600;
|
||
margin-bottom: 0.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.card-description {
|
||
color: var(--muted-foreground);
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
/* 现代化表单设计 */
|
||
.form-group {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
font-weight: 500;
|
||
margin-bottom: 0.5rem;
|
||
color: var(--foreground);
|
||
}
|
||
|
||
.input {
|
||
width: 100%;
|
||
padding: 0.75rem 1rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background-color: var(--input);
|
||
color: var(--foreground);
|
||
font-size: 1rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.input:focus {
|
||
outline: none;
|
||
border-color: var(--ring);
|
||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||
}
|
||
|
||
.input::placeholder {
|
||
color: var(--muted-foreground);
|
||
}
|
||
|
||
.textarea {
|
||
min-height: 120px;
|
||
resize: vertical;
|
||
font-family: 'Consolas', 'Monaco', monospace;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.select {
|
||
padding: 0.75rem 1rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background-color: var(--input);
|
||
color: var(--foreground);
|
||
font-size: 1rem;
|
||
transition: all 0.2s;
|
||
min-width: 120px;
|
||
}
|
||
|
||
.select:focus {
|
||
outline: none;
|
||
border-color: var(--ring);
|
||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||
}
|
||
|
||
.button {
|
||
padding: 0.75rem 1.5rem;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.button-primary {
|
||
background-color: var(--primary);
|
||
color: var(--primary-foreground);
|
||
}
|
||
|
||
.button-primary:hover {
|
||
background-color: #1d4ed8;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.button-secondary {
|
||
background-color: var(--secondary);
|
||
color: var(--secondary-foreground);
|
||
}
|
||
|
||
.button-secondary:hover {
|
||
background-color: var(--muted);
|
||
}
|
||
|
||
.button-success {
|
||
background-color: var(--success);
|
||
color: white;
|
||
}
|
||
|
||
.button-success:hover {
|
||
background-color: #047857;
|
||
}
|
||
|
||
.button-warning {
|
||
background-color: var(--warning);
|
||
color: white;
|
||
}
|
||
|
||
.button-warning:hover {
|
||
background-color: #b45309;
|
||
}
|
||
|
||
.button-error {
|
||
background-color: var(--error);
|
||
color: white;
|
||
}
|
||
|
||
.button-error:hover {
|
||
background-color: #b91c1c;
|
||
}
|
||
|
||
.button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
/* 架构选择区域 */
|
||
.arch-buttons {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.arch-button {
|
||
padding: 0.5rem 1rem;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background-color: var(--card);
|
||
color: var(--foreground);
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.arch-button:hover,
|
||
.arch-button.selected {
|
||
background-color: var(--primary);
|
||
color: var(--primary-foreground);
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
/* 进度显示 */
|
||
.progress-container {
|
||
display: none;
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.progress-container.show {
|
||
display: block;
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.progress-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.progress-title {
|
||
font-weight: 600;
|
||
color: var(--foreground);
|
||
}
|
||
|
||
.progress-stats {
|
||
font-size: 0.875rem;
|
||
color: var(--muted-foreground);
|
||
}
|
||
|
||
.progress-bar-container {
|
||
background-color: var(--muted);
|
||
border-radius: var(--radius);
|
||
height: 8px;
|
||
overflow: hidden;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--primary), #3b82f6);
|
||
border-radius: var(--radius);
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
/* 任务列表 */
|
||
.task-list {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background-color: var(--card);
|
||
}
|
||
|
||
.task-item {
|
||
padding: 1rem;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.task-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.task-item:hover {
|
||
background-color: var(--muted);
|
||
}
|
||
|
||
.task-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.task-name {
|
||
font-weight: 500;
|
||
margin-bottom: 0.25rem;
|
||
font-family: 'Consolas', 'Monaco', monospace;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.task-status {
|
||
font-size: 0.75rem;
|
||
color: var(--muted-foreground);
|
||
}
|
||
|
||
.task-badge {
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 0.25rem;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.task-badge.pending {
|
||
background-color: var(--muted);
|
||
color: var(--muted-foreground);
|
||
}
|
||
|
||
.task-badge.running {
|
||
background-color: var(--warning);
|
||
color: white;
|
||
}
|
||
|
||
.task-badge.completed {
|
||
background-color: var(--success);
|
||
color: white;
|
||
}
|
||
|
||
.task-badge.failed {
|
||
background-color: var(--error);
|
||
color: white;
|
||
}
|
||
|
||
/* Toast 通知 */
|
||
.toast {
|
||
position: fixed;
|
||
top: 1rem;
|
||
right: 1rem;
|
||
background-color: var(--primary);
|
||
color: var(--primary-foreground);
|
||
padding: 1rem 1.5rem;
|
||
border-radius: var(--radius);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
z-index: 1001;
|
||
display: none;
|
||
opacity: 0;
|
||
transform: translateX(100%);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.toast.show {
|
||
display: block;
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.toast.success {
|
||
background-color: var(--success);
|
||
}
|
||
|
||
.toast.error {
|
||
background-color: var(--error);
|
||
}
|
||
|
||
.toast.warning {
|
||
background-color: var(--warning);
|
||
}
|
||
|
||
/* 页脚 */
|
||
.footer {
|
||
padding: 2rem 1rem;
|
||
text-align: center;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.github-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
color: var(--muted-foreground);
|
||
text-decoration: none;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.github-link:hover {
|
||
color: var(--foreground);
|
||
}
|
||
|
||
/* 动画 */
|
||
@keyframes fadeInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.hero-title {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.nav-links {
|
||
display: none;
|
||
}
|
||
|
||
.input-group {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.arch-buttons {
|
||
justify-content: center;
|
||
}
|
||
|
||
.task-item {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 0.5rem;
|
||
}
|
||
}
|
||
</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">
|
||
<!-- Hero 区域 -->
|
||
<div class="hero">
|
||
<h1 class="hero-title">Docker 镜像批量下载</h1>
|
||
<p class="hero-subtitle">
|
||
批量下载和打包Docker镜像,支持多架构和自定义标签选择
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 下载配置卡片 -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h2 class="card-title">
|
||
📦 下载配置
|
||
</h2>
|
||
<p class="card-description">
|
||
设置要下载的镜像列表、架构和输出格式
|
||
</p>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">镜像列表</label>
|
||
<textarea
|
||
class="input textarea"
|
||
id="imageListInput"
|
||
placeholder="每行一个镜像,例如: nginx:latest mysql:8.0 redis:alpine ubuntu:20.04"
|
||
></textarea>
|
||
<small style="color: var(--muted-foreground); font-size: 0.875rem; margin-top: 0.5rem; display: block;">
|
||
每行输入一个镜像名称,支持标签。例如:nginx:latest, mysql:8.0, redis:alpine
|
||
</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">架构选择</label>
|
||
<div class="input-group">
|
||
<input
|
||
type="text"
|
||
class="input"
|
||
id="architectureInput"
|
||
placeholder="输入架构,例如:linux/amd64"
|
||
value="linux/amd64"
|
||
>
|
||
</div>
|
||
<div class="arch-buttons">
|
||
<button class="arch-button" data-arch="linux/amd64">linux/amd64</button>
|
||
<button class="arch-button" data-arch="linux/arm64">linux/arm64</button>
|
||
<button class="arch-button" data-arch="linux/arm/v7">linux/arm/v7</button>
|
||
<button class="arch-button" data-arch="linux/386">linux/386</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">输出格式</label>
|
||
<select class="select" id="formatSelect">
|
||
<option value="docker-archive">Docker Archive (.tar)</option>
|
||
<option value="oci-archive">OCI Archive (.tar)</option>
|
||
<option value="dir">目录格式</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||
<button class="button button-primary" id="startDownload">
|
||
🚀 开始下载
|
||
</button>
|
||
<button class="button button-warning" id="pauseDownload" disabled>
|
||
⏸️ 暂停下载
|
||
</button>
|
||
<button class="button button-error" id="stopDownload" disabled>
|
||
⏹️ 停止下载
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 进度显示卡片 -->
|
||
<div class="card progress-container" id="progressContainer">
|
||
<div class="card-header">
|
||
<h3 class="card-title">
|
||
📈 下载进度
|
||
</h3>
|
||
</div>
|
||
|
||
<div class="progress-header">
|
||
<div class="progress-title" id="progressTitle">准备中...</div>
|
||
<div class="progress-stats" id="progressStats">0/0</div>
|
||
</div>
|
||
|
||
<div class="progress-bar-container">
|
||
<div class="progress-bar" id="progressBar" style="width: 0%"></div>
|
||
</div>
|
||
|
||
<div class="task-list" id="taskList">
|
||
<!-- 任务项将动态插入 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Toast 通知 -->
|
||
<div id="toast" class="toast"></div>
|
||
|
||
<!-- 页脚 -->
|
||
<footer class="footer">
|
||
<a href="https://github.com/sky22333/hubproxy" target="_blank" class="github-link">
|
||
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
|
||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||
</svg>
|
||
GitHub
|
||
</a>
|
||
</footer>
|
||
|
||
<script>
|
||
// 主题管理
|
||
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');
|
||
});
|
||
|
||
// 全局变量
|
||
let downloadTasks = [];
|
||
let isDownloading = false;
|
||
let currentTaskIndex = 0;
|
||
let downloadInterval = null;
|
||
|
||
// DOM 元素
|
||
const elements = {
|
||
imageListInput: document.getElementById('imageListInput'),
|
||
architectureInput: document.getElementById('architectureInput'),
|
||
formatSelect: document.getElementById('formatSelect'),
|
||
startDownload: document.getElementById('startDownload'),
|
||
pauseDownload: document.getElementById('pauseDownload'),
|
||
stopDownload: document.getElementById('stopDownload'),
|
||
progressContainer: document.getElementById('progressContainer'),
|
||
progressTitle: document.getElementById('progressTitle'),
|
||
progressStats: document.getElementById('progressStats'),
|
||
progressBar: document.getElementById('progressBar'),
|
||
taskList: document.getElementById('taskList'),
|
||
toast: document.getElementById('toast')
|
||
};
|
||
|
||
// Toast 通知
|
||
function showToast(message, type = 'info') {
|
||
const toast = elements.toast;
|
||
toast.textContent = message;
|
||
toast.className = `toast show ${type}`;
|
||
|
||
setTimeout(() => {
|
||
toast.classList.remove('show');
|
||
}, 3000);
|
||
}
|
||
|
||
// 架构按钮选择
|
||
document.querySelectorAll('.arch-button').forEach(button => {
|
||
button.addEventListener('click', () => {
|
||
// 移除其他按钮的选中状态
|
||
document.querySelectorAll('.arch-button').forEach(btn => {
|
||
btn.classList.remove('selected');
|
||
});
|
||
|
||
// 选中当前按钮
|
||
button.classList.add('selected');
|
||
elements.architectureInput.value = button.dataset.arch;
|
||
});
|
||
});
|
||
|
||
// 默认选中第一个架构
|
||
document.querySelector('.arch-button[data-arch="linux/amd64"]').classList.add('selected');
|
||
|
||
// 解析镜像列表
|
||
function parseImageList(input) {
|
||
return input.split('\n')
|
||
.map(line => line.trim())
|
||
.filter(line => line && !line.startsWith('#'))
|
||
.map(line => {
|
||
// 基本格式验证
|
||
if (!line.includes('/') && !line.includes(':')) {
|
||
return line + ':latest'; // 默认添加 latest 标签
|
||
}
|
||
return line;
|
||
});
|
||
}
|
||
|
||
// 创建任务项
|
||
function createTaskItem(imageName, index) {
|
||
const item = document.createElement('div');
|
||
item.className = 'task-item';
|
||
item.id = `task-${index}`;
|
||
|
||
item.innerHTML = `
|
||
<div class="task-info">
|
||
<div class="task-name">${imageName}</div>
|
||
<div class="task-status">等待开始...</div>
|
||
</div>
|
||
<div class="task-badge pending">等待中</div>
|
||
`;
|
||
|
||
return item;
|
||
}
|
||
|
||
// 更新任务状态
|
||
function updateTaskStatus(index, status, message = '') {
|
||
const taskItem = document.getElementById(`task-${index}`);
|
||
if (!taskItem) return;
|
||
|
||
const statusElement = taskItem.querySelector('.task-status');
|
||
const badgeElement = taskItem.querySelector('.task-badge');
|
||
|
||
statusElement.textContent = message || getStatusMessage(status);
|
||
|
||
badgeElement.className = `task-badge ${status}`;
|
||
badgeElement.textContent = getStatusText(status);
|
||
}
|
||
|
||
function getStatusMessage(status) {
|
||
switch (status) {
|
||
case 'pending': return '等待开始...';
|
||
case 'running': return '正在下载...';
|
||
case 'completed': return '下载完成';
|
||
case 'failed': return '下载失败';
|
||
default: return '';
|
||
}
|
||
}
|
||
|
||
function getStatusText(status) {
|
||
switch (status) {
|
||
case 'pending': return '等待中';
|
||
case 'running': return '进行中';
|
||
case 'completed': return '已完成';
|
||
case 'failed': return '失败';
|
||
default: return '';
|
||
}
|
||
}
|
||
|
||
// 更新进度
|
||
function updateProgress() {
|
||
const total = downloadTasks.length;
|
||
const completed = downloadTasks.filter(task =>
|
||
task.status === 'completed' || task.status === 'failed'
|
||
).length;
|
||
|
||
const percentage = total > 0 ? (completed / total) * 100 : 0;
|
||
|
||
elements.progressStats.textContent = `${completed}/${total}`;
|
||
elements.progressBar.style.width = `${percentage}%`;
|
||
|
||
if (completed === total && total > 0) {
|
||
elements.progressTitle.textContent = '下载完成';
|
||
const successCount = downloadTasks.filter(task => task.status === 'completed').length;
|
||
showToast(`所有任务已完成!成功:${successCount},失败:${total - successCount}`, 'success');
|
||
stopDownload();
|
||
} else if (isDownloading) {
|
||
elements.progressTitle.textContent = `正在下载 (${completed + 1}/${total})`;
|
||
}
|
||
}
|
||
|
||
// 开始下载
|
||
async function startDownload() {
|
||
const imageList = parseImageList(elements.imageListInput.value);
|
||
const architecture = elements.architectureInput.value.trim();
|
||
const format = elements.formatSelect.value;
|
||
|
||
if (imageList.length === 0) {
|
||
showToast('请输入要下载的镜像列表', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!architecture) {
|
||
showToast('请选择架构', 'error');
|
||
return;
|
||
}
|
||
|
||
// 初始化任务
|
||
downloadTasks = imageList.map((image, index) => ({
|
||
id: index,
|
||
image: image,
|
||
architecture: architecture,
|
||
format: format,
|
||
status: 'pending'
|
||
}));
|
||
|
||
// 清空任务列表并重新创建
|
||
elements.taskList.innerHTML = '';
|
||
downloadTasks.forEach((task, index) => {
|
||
const taskItem = createTaskItem(task.image, index);
|
||
elements.taskList.appendChild(taskItem);
|
||
});
|
||
|
||
// 显示进度容器
|
||
elements.progressContainer.classList.add('show');
|
||
|
||
// 更新按钮状态
|
||
elements.startDownload.disabled = true;
|
||
elements.pauseDownload.disabled = false;
|
||
elements.stopDownload.disabled = false;
|
||
|
||
isDownloading = true;
|
||
currentTaskIndex = 0;
|
||
|
||
elements.progressTitle.textContent = '开始下载...';
|
||
showToast('开始批量下载镜像', 'success');
|
||
|
||
// 开始处理任务
|
||
processNextTask();
|
||
}
|
||
|
||
// 处理下一个任务
|
||
async function processNextTask() {
|
||
if (!isDownloading || currentTaskIndex >= downloadTasks.length) {
|
||
return;
|
||
}
|
||
|
||
const task = downloadTasks[currentTaskIndex];
|
||
|
||
// 更新任务状态为运行中
|
||
task.status = 'running';
|
||
updateTaskStatus(currentTaskIndex, 'running', '正在下载...');
|
||
updateProgress();
|
||
|
||
try {
|
||
// 调用后端API开始下载
|
||
const response = await fetch('/api/skopeo/download', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
image: task.image,
|
||
architecture: task.architecture,
|
||
format: task.format
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
task.status = 'completed';
|
||
updateTaskStatus(currentTaskIndex, 'completed', '下载完成');
|
||
} else {
|
||
task.status = 'failed';
|
||
updateTaskStatus(currentTaskIndex, 'failed', result.message || '下载失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('下载错误:', error);
|
||
task.status = 'failed';
|
||
updateTaskStatus(currentTaskIndex, 'failed', '网络错误');
|
||
}
|
||
|
||
updateProgress();
|
||
currentTaskIndex++;
|
||
|
||
// 继续下一个任务
|
||
if (isDownloading && currentTaskIndex < downloadTasks.length) {
|
||
setTimeout(() => processNextTask(), 1000); // 延迟1秒继续下一个
|
||
}
|
||
}
|
||
|
||
// 暂停下载
|
||
function pauseDownload() {
|
||
isDownloading = false;
|
||
elements.startDownload.disabled = false;
|
||
elements.pauseDownload.disabled = true;
|
||
elements.progressTitle.textContent = '下载已暂停';
|
||
showToast('下载已暂停', 'warning');
|
||
}
|
||
|
||
// 停止下载
|
||
function stopDownload() {
|
||
isDownloading = false;
|
||
currentTaskIndex = 0;
|
||
|
||
elements.startDownload.disabled = false;
|
||
elements.pauseDownload.disabled = true;
|
||
elements.stopDownload.disabled = true;
|
||
|
||
if (downloadTasks.length > 0) {
|
||
elements.progressTitle.textContent = '下载已停止';
|
||
}
|
||
}
|
||
|
||
// 恢复下载
|
||
function resumeDownload() {
|
||
if (downloadTasks.length === 0) {
|
||
showToast('没有可恢复的任务', 'error');
|
||
return;
|
||
}
|
||
|
||
isDownloading = true;
|
||
elements.startDownload.disabled = true;
|
||
elements.pauseDownload.disabled = false;
|
||
elements.stopDownload.disabled = false;
|
||
|
||
elements.progressTitle.textContent = '恢复下载...';
|
||
showToast('恢复下载', 'success');
|
||
|
||
processNextTask();
|
||
}
|
||
|
||
// 事件监听
|
||
elements.startDownload.addEventListener('click', () => {
|
||
if (downloadTasks.length > 0 && currentTaskIndex < downloadTasks.length) {
|
||
resumeDownload();
|
||
} else {
|
||
startDownload();
|
||
}
|
||
});
|
||
|
||
elements.pauseDownload.addEventListener('click', pauseDownload);
|
||
elements.stopDownload.addEventListener('click', stopDownload);
|
||
|
||
// 页面初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// 示例镜像列表
|
||
elements.imageListInput.value = `nginx:latest
|
||
mysql:8.0
|
||
redis:alpine
|
||
ubuntu:20.04`;
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |