mirror of
https://github.com/sky22333/hubproxy.git
synced 2026-08-28 19:46:44 +08:00
789 lines
25 KiB
HTML
789 lines
25 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镜像流式下载工具,即点即下,无需等待">
|
|
<meta name="keywords" content="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: #10b981;
|
|
--warning: #f59e0b;
|
|
--error: #ef4444;
|
|
}
|
|
|
|
.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: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(8px);
|
|
padding: 0;
|
|
}
|
|
|
|
.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: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 3rem;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 1.125rem;
|
|
color: var(--muted-foreground);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.features {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 1rem;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.feature {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 1rem;
|
|
background-color: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.feature-icon {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
/* 下载区域 */
|
|
.download-section,
|
|
.batch-section {
|
|
background-color: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1.5rem;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
font-weight: 500;
|
|
margin-bottom: 0.5rem;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.form-input,
|
|
.form-select,
|
|
.textarea {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background-color: var(--input);
|
|
color: var(--foreground);
|
|
font-size: 1rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.form-input:focus,
|
|
.form-select:focus,
|
|
.textarea:focus {
|
|
outline: none;
|
|
border-color: var(--ring);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
}
|
|
|
|
.textarea {
|
|
min-height: 120px;
|
|
resize: vertical;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1.5rem;
|
|
border: none;
|
|
border-radius: var(--radius);
|
|
font-weight: 500;
|
|
font-size: 1rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: var(--primary);
|
|
color: var(--primary-foreground);
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background-color: #1d4ed8;
|
|
}
|
|
|
|
.btn-primary:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-full {
|
|
width: 100%;
|
|
}
|
|
|
|
.status {
|
|
padding: 1rem;
|
|
border-radius: var(--radius);
|
|
margin-bottom: 1rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-success {
|
|
background-color: rgba(16, 185, 129, 0.1);
|
|
color: var(--success);
|
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
|
}
|
|
|
|
.status-error {
|
|
background-color: rgba(239, 68, 68, 0.1);
|
|
color: var(--error);
|
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
}
|
|
|
|
.status-warning {
|
|
background-color: rgba(245, 158, 11, 0.1);
|
|
color: var(--warning);
|
|
border: 1px solid rgba(245, 158, 11, 0.2);
|
|
}
|
|
|
|
.help-text {
|
|
font-size: 0.875rem;
|
|
color: var(--muted-foreground);
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.navbar-container {
|
|
padding: 0 0.5rem;
|
|
}
|
|
|
|
.nav-links {
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.nav-link {
|
|
padding: 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.main {
|
|
padding: 1rem 0.5rem;
|
|
}
|
|
|
|
.download-section,
|
|
.batch-section {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.form-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.features {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.title {
|
|
font-size: 2rem;
|
|
}
|
|
}
|
|
|
|
.loading {
|
|
display: inline-block;
|
|
width: 1rem;
|
|
height: 1rem;
|
|
border: 2px solid transparent;
|
|
border-top: 2px solid currentColor;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.mobile-menu-toggle {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.navbar-container {
|
|
padding: 0 0.5rem;
|
|
}
|
|
|
|
.nav-links {
|
|
position: fixed;
|
|
top: 70px;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--background);
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
border-radius: 0 0 12px 12px;
|
|
padding: 1rem;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
z-index: 1000;
|
|
transform: translateY(-100vh);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.nav-links.active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.mobile-menu-toggle {
|
|
display: block !important;
|
|
background: none;
|
|
border: none;
|
|
color: var(--foreground);
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
padding: 0.5rem;
|
|
border-radius: var(--radius);
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.mobile-menu-toggle:hover {
|
|
background-color: var(--muted);
|
|
}
|
|
|
|
.navbar-container {
|
|
justify-content: space-between !important;
|
|
}
|
|
|
|
.main {
|
|
padding: 1rem 0.5rem;
|
|
}
|
|
|
|
.download-section,
|
|
.batch-section {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.form-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.features {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.title {
|
|
font-size: 2rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="navbar">
|
|
<div class="navbar-container">
|
|
<a href="/" class="logo">
|
|
<div class="logo-icon">
|
|
⚡
|
|
</div>
|
|
加速服务
|
|
</a>
|
|
|
|
<button class="mobile-menu-toggle" id="mobileMenuToggle">
|
|
☰
|
|
</button>
|
|
|
|
<div class="nav-links" id="navLinks">
|
|
<a href="/" class="nav-link">🚀 GitHub加速</a>
|
|
<a href="/images.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">
|
|
<div class="header">
|
|
<h1 class="title">Docker离线镜像下载</h1>
|
|
<p class="subtitle">即点即下,无需等待打包,完全符合docker load加载标准</p>
|
|
|
|
<div class="features">
|
|
<div class="feature">
|
|
<span class="feature-icon">⚡</span>
|
|
<span>即时下载</span>
|
|
</div>
|
|
<div class="feature">
|
|
<span class="feature-icon">🔄</span>
|
|
<span>流式传输</span>
|
|
</div>
|
|
<div class="feature">
|
|
<span class="feature-icon">💾</span>
|
|
<span>无需打包</span>
|
|
</div>
|
|
<div class="feature">
|
|
<span class="feature-icon">🏗️</span>
|
|
<span>多架构支持</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="download-section">
|
|
<h2 class="section-title">单镜像下载</h2>
|
|
|
|
<div id="singleStatus"></div>
|
|
|
|
<form id="singleForm">
|
|
<div class="form-group">
|
|
<label class="form-label" for="imageInput">镜像名称</label>
|
|
<input
|
|
type="text"
|
|
id="imageInput"
|
|
class="form-input"
|
|
placeholder="例如: nginx:alpine"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="platformInput">目标架构(可选)</label>
|
|
<input
|
|
type="text"
|
|
id="platformInput"
|
|
class="form-input"
|
|
placeholder="linux/amd64"
|
|
value="linux/amd64"
|
|
>
|
|
<div class="help-text">
|
|
常用平台: linux/amd64, linux/arm64, linux/arm/v7
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary btn-full" id="downloadBtn">
|
|
<span id="downloadText">立即下载</span>
|
|
<span id="downloadLoading" class="loading hidden"></span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="batch-section">
|
|
<h2 class="section-title">多个镜像批量下载</h2>
|
|
|
|
<div id="batchStatus"></div>
|
|
|
|
<form id="batchForm">
|
|
<div class="form-group">
|
|
<label class="form-label" for="imagesTextarea">镜像列表,每行一个,会将多个镜像自动合并,符合官方标准,完全兼容docker load</label>
|
|
<textarea
|
|
id="imagesTextarea"
|
|
class="textarea"
|
|
placeholder="alpine redis:alpine stilleshan/frpc:0.62.1"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="batchPlatformInput">目标架构(可选)</label>
|
|
<input
|
|
type="text"
|
|
id="batchPlatformInput"
|
|
class="form-input"
|
|
placeholder="linux/amd64"
|
|
value="linux/amd64"
|
|
>
|
|
<div class="help-text">
|
|
所有镜像将使用相同的目标架构
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary btn-full" id="batchDownloadBtn">
|
|
<span id="batchDownloadText">开始下载</span>
|
|
<span id="batchDownloadLoading" class="loading hidden"></span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
function initTheme() {
|
|
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');
|
|
});
|
|
}
|
|
|
|
function showStatus(elementId, message, type = 'success') {
|
|
const element = document.getElementById(elementId);
|
|
element.className = `status status-${type}`;
|
|
element.textContent = message;
|
|
element.classList.remove('hidden');
|
|
}
|
|
|
|
function hideStatus(elementId) {
|
|
document.getElementById(elementId).classList.add('hidden');
|
|
}
|
|
|
|
function setButtonLoading(btnId, textId, loadingId, loading) {
|
|
const btn = document.getElementById(btnId);
|
|
const text = document.getElementById(textId);
|
|
const loadingSpinner = document.getElementById(loadingId);
|
|
|
|
btn.disabled = loading;
|
|
if (loading) {
|
|
text.classList.add('hidden');
|
|
loadingSpinner.classList.remove('hidden');
|
|
} else {
|
|
text.classList.remove('hidden');
|
|
loadingSpinner.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function buildDownloadUrl(imageName, platform = '') {
|
|
const encodedImage = imageName.replace(/\//g, '_');
|
|
let url = `/api/image/download/${encodedImage}`;
|
|
|
|
if (platform && platform.trim()) {
|
|
url += `?platform=${encodeURIComponent(platform.trim())}`;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
document.getElementById('singleForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
const imageName = document.getElementById('imageInput').value.trim();
|
|
if (!imageName) {
|
|
showStatus('singleStatus', '请输入镜像名称', 'error');
|
|
return;
|
|
}
|
|
|
|
const platform = document.getElementById('platformInput').value.trim();
|
|
|
|
hideStatus('singleStatus');
|
|
setButtonLoading('downloadBtn', 'downloadText', 'downloadLoading', true);
|
|
|
|
const downloadUrl = buildDownloadUrl(imageName, platform);
|
|
|
|
const link = document.createElement('a');
|
|
link.href = downloadUrl;
|
|
link.download = '';
|
|
link.style.display = 'none';
|
|
document.body.appendChild(link);
|
|
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
const platformText = platform ? ` (${platform})` : '';
|
|
showStatus('singleStatus', `开始下载 ${imageName}${platformText}`, 'success');
|
|
setButtonLoading('downloadBtn', 'downloadText', 'downloadLoading', false);
|
|
});
|
|
|
|
document.getElementById('batchForm').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const imagesText = document.getElementById('imagesTextarea').value.trim();
|
|
if (!imagesText) {
|
|
showStatus('batchStatus', '请输入镜像列表', 'error');
|
|
return;
|
|
}
|
|
|
|
const images = imagesText.split('\n')
|
|
.map(line => line.trim())
|
|
.filter(line => line && !line.startsWith('#'));
|
|
|
|
if (images.length === 0) {
|
|
showStatus('batchStatus', '镜像列表为空', 'error');
|
|
return;
|
|
}
|
|
|
|
const platform = document.getElementById('batchPlatformInput').value.trim();
|
|
|
|
const options = {
|
|
images: images
|
|
};
|
|
|
|
if (platform) {
|
|
options.platform = platform;
|
|
}
|
|
|
|
hideStatus('batchStatus');
|
|
setButtonLoading('batchDownloadBtn', 'batchDownloadText', 'batchDownloadLoading', true);
|
|
|
|
try {
|
|
const response = await fetch('/api/image/batch', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(options)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
let filename = `batch_${images.length}_images.tar`;
|
|
|
|
if (contentDisposition) {
|
|
const matches = contentDisposition.match(/filename="(.+)"/);
|
|
if (matches) filename = matches[1];
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
link.style.display = 'none';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
const platformText = platform ? ` (${platform})` : '';
|
|
showStatus('batchStatus', `开始下载 ${images.length} 个镜像${platformText}`, 'success');
|
|
} else {
|
|
const error = await response.json();
|
|
showStatus('batchStatus', error.error || '下载失败', 'error');
|
|
}
|
|
} catch (error) {
|
|
showStatus('batchStatus', '网络错误: ' + error.message, 'error');
|
|
} finally {
|
|
setButtonLoading('batchDownloadBtn', 'batchDownloadText', 'batchDownloadLoading', false);
|
|
}
|
|
});
|
|
|
|
function initMobileMenu() {
|
|
const mobileMenuToggle = document.getElementById('mobileMenuToggle');
|
|
const navLinks = document.getElementById('navLinks');
|
|
|
|
if (mobileMenuToggle && navLinks) {
|
|
mobileMenuToggle.addEventListener('click', () => {
|
|
navLinks.classList.toggle('active');
|
|
});
|
|
|
|
navLinks.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('nav-link')) {
|
|
navLinks.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
initTheme();
|
|
initMobileMenu();
|
|
</script>
|
|
</body>
|
|
</html> |