Files
hubproxy/src/public/skopeo.html
2025-06-12 12:44:46 +08:00

1061 lines
34 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;
--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="每行一个镜像,例如:&#10;nginx:latest&#10;mysql:8.0&#10;redis:alpine&#10;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>