mirror of
https://github.com/dqzboy/Docker-Proxy.git
synced 2026-07-04 06:55:24 +08:00
1691 lines
77 KiB
HTML
1691 lines
77 KiB
HTML
<!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">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<link rel="stylesheet" href="style.css">
|
|
<script src="js/nav-menu.js"></script>
|
|
</head>
|
|
<body>
|
|
<header class="header">
|
|
<div class="header-content">
|
|
<a href="/" class="logo-link">
|
|
<img src="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" alt="Logo" class="logo">
|
|
</a>
|
|
<nav class="nav-menu" id="navMenu">
|
|
<!-- 菜单项通过 JavaScript 动态加载 -->
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="container">
|
|
<h1 class="page-title">Docker镜像加速服务</h1>
|
|
<p class="page-subtitle">快速拉取 Docker 镜像,无需担心网络问题,轻松部署你的容器应用</p>
|
|
|
|
<div class="tab-container">
|
|
<div class="tab active" onclick="switchTab('accelerate')">
|
|
<i class="fas fa-rocket"></i> 镜像加速
|
|
</div>
|
|
<div class="tab" onclick="switchTab('search')">
|
|
<i class="fas fa-search"></i> 镜像搜索
|
|
</div>
|
|
<div class="tab" onclick="switchTab('documentation')">
|
|
<i class="fas fa-book"></i> 使用教程
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 镜像加速内容 -->
|
|
<div id="accelerateContent" class="content active">
|
|
<div class="input-group">
|
|
<input type="text" id="imageInput"
|
|
placeholder="输入镜像名称,例如:nginx 或 mysql:5.7"
|
|
onkeypress="if(event.key === 'Enter') generateCommands()"
|
|
autofocus>
|
|
<button onclick="generateCommands()">
|
|
<i class="fas fa-bolt"></i> 获取加速命令
|
|
</button>
|
|
</div>
|
|
|
|
<div id="result" style="display:none;">
|
|
<h2><i class="fas fa-terminal"></i> 加速命令</h2>
|
|
<div id="commandsContainer"></div>
|
|
</div>
|
|
|
|
<div class="features">
|
|
<div class="feature-card">
|
|
<i class="fas fa-tachometer-alt"></i>
|
|
<h3>高速拉取</h3>
|
|
<p>通过优化的代理网络,加速Docker镜像拉取</p>
|
|
</div>
|
|
<div class="feature-card">
|
|
<i class="fas fa-shield-alt"></i>
|
|
<h3>稳定可靠</h3>
|
|
<p>解决网络问题导致的拉取失败,提高部署成功率</p>
|
|
</div>
|
|
<div class="feature-card">
|
|
<i class="fas fa-magic"></i>
|
|
<h3>简单易用</h3>
|
|
<p>一键生成加速命令,无需复杂配置,立即开始使用</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 搜索内容 -->
|
|
<div id="searchContent" class="content">
|
|
<div class="search-container">
|
|
<input type="text" id="searchInput"
|
|
placeholder="输入关键词搜索Docker镜像,例如:nginx、mysql、redis..."
|
|
onkeypress="if(event.key === 'Enter') searchDockerHub(1)">
|
|
<button onclick="searchDockerHub(1)">
|
|
<i class="fas fa-search"></i> 搜索镜像
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 搜索结果容器 -->
|
|
<div id="searchResultsContainer">
|
|
<!-- 搜索结果列表 -->
|
|
<div id="searchResultsList">
|
|
<div id="searchResults"></div>
|
|
|
|
<!-- 分页控件 -->
|
|
<div class="pagination-container" id="paginationContainer" style="display: none;">
|
|
<button id="prevPageBtn" onclick="searchDockerHub(currentPage - 1)" disabled>
|
|
<i class="fas fa-chevron-left"></i> 上一页
|
|
</button>
|
|
<span id="pageInfo">第 1 页</span>
|
|
<button id="nextPageBtn" onclick="searchDockerHub(currentPage + 1)">
|
|
下一页 <i class="fas fa-chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 标签视图 -->
|
|
<div id="imageTagsView" style="display: none;" class="image-tags-view">
|
|
<div class="tag-header">
|
|
<div class="tag-breadcrumb">
|
|
<a href="javascript:void(0);" onclick="showSearchResults()">返回搜索结果</a>
|
|
</div>
|
|
<h2 id="currentImageTitle"></h2>
|
|
<p id="imageDescription" class="image-description"></p>
|
|
<div class="image-meta">
|
|
<span id="imageStars"></span>
|
|
<span id="imagePulls"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tag-search-container">
|
|
<input type="text" id="tagSearchInput" placeholder="搜索TAG..." onkeyup="filterTags()">
|
|
</div>
|
|
|
|
<div id="tagsResults"></div>
|
|
|
|
<div class="pagination-container" id="tagPaginationContainer" style="display: none;">
|
|
<button id="tagPrevPageBtn" onclick="loadImageTags(currentTagPage - 1)" disabled>
|
|
<i class="fas fa-chevron-left"></i> 上一页
|
|
</button>
|
|
<span id="tagPageInfo">第 1 页</span>
|
|
<button id="tagNextPageBtn" onclick="loadImageTags(currentTagPage + 1)">
|
|
下一页 <i class="fas fa-chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 底部特性说明 -->
|
|
<div class="features">
|
|
<div class="feature-card">
|
|
<i class="fas fa-search"></i>
|
|
<h3>快速搜索</h3>
|
|
<p>便捷地搜索Docker Hub上的所有可用镜像</p>
|
|
</div>
|
|
<div class="feature-card">
|
|
<i class="fas fa-tag"></i>
|
|
<h3>版本管理</h3>
|
|
<p>查看所有可用的镜像标签和版本信息</p>
|
|
</div>
|
|
<div class="feature-card">
|
|
<i class="fas fa-rocket"></i>
|
|
<h3>一键部署</h3>
|
|
<p>快速获取并使用所需的Docker镜像</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 文档内容 -->
|
|
<div id="documentationContent" class="content">
|
|
<div id="documentList"></div>
|
|
<div id="documentationText"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer class="footer">
|
|
<p>Copyright © <span id="currentYear"></span> <span class="copyright-text">Docker-Proxy</span> All Rights Reserved. <a href="https://github.com/dqzboy/Docker-Proxy" target="_blank">GitHub</a></p>
|
|
</footer>
|
|
|
|
<script>
|
|
// 设置当前年份
|
|
document.getElementById('currentYear').textContent = new Date().getFullYear();
|
|
document.addEventListener('DOMContentLoaded', (event) => {
|
|
// 版权保护
|
|
protectCopyright();
|
|
});
|
|
|
|
// 版权保护函数
|
|
function protectCopyright() {
|
|
const footer = document.querySelector('.footer');
|
|
const expectedText = 'Docker-Proxy';
|
|
const expectedLink = 'https://github.com/dqzboy/Docker-Proxy';
|
|
|
|
// 初始检查
|
|
validateCopyright();
|
|
|
|
// 定期检查版权信息
|
|
setInterval(validateCopyright, 2000);
|
|
|
|
function validateCopyright() {
|
|
const copyrightText = document.querySelector('.copyright-text');
|
|
const githubLink = document.querySelector('.footer a');
|
|
|
|
if (!copyrightText || copyrightText.textContent !== expectedText ||
|
|
!githubLink || githubLink.href !== expectedLink) {
|
|
// 版权信息被篡改,恢复
|
|
restoreCopyright();
|
|
}
|
|
}
|
|
|
|
function restoreCopyright() {
|
|
footer.innerHTML = `<p>Copyright © <span id="currentYear">${new Date().getFullYear()}</span> <span class="copyright-text">Docker-Proxy</span> All Rights Reserved. <a href="https://github.com/dqzboy/Docker-Proxy" target="_blank">GitHub</a></p>`;
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// === 文档加载相关函数 (移到此处) ===
|
|
// ========================================
|
|
let documentationLoaded = false;
|
|
async function loadAndDisplayDocumentation() {
|
|
// 防止重复加载
|
|
if (documentationLoaded) {
|
|
console.log('文档已加载,跳过重复加载');
|
|
return;
|
|
}
|
|
|
|
const docListContainer = document.getElementById('documentList');
|
|
const docContentContainer = document.getElementById('documentationText');
|
|
|
|
if (!docListContainer || !docContentContainer) {
|
|
console.warn('找不到文档列表或内容容器,可能不是文档页面');
|
|
return; // 如果容器不存在,则不执行加载
|
|
}
|
|
|
|
try {
|
|
console.log('开始加载文档列表和内容...');
|
|
|
|
// 显示加载状态
|
|
docListContainer.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 正在加载文档列表...</div>';
|
|
docContentContainer.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 请从左侧选择文档...</div>';
|
|
|
|
// 获取文档列表
|
|
const response = await fetch('/api/documentation');
|
|
if (!response.ok) {
|
|
throw new Error(`获取文档列表失败: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('获取到文档列表:', data);
|
|
|
|
// 保存到全局变量
|
|
window.documentationData = data;
|
|
documentationLoaded = true; // 标记为已加载
|
|
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
docListContainer.innerHTML = `
|
|
<h2>文档目录</h2>
|
|
<div class="empty-list">
|
|
<i class="fas fa-file-alt fa-3x"></i>
|
|
<p>暂无文档</p>
|
|
</div>
|
|
`;
|
|
docContentContainer.innerHTML = `
|
|
<div class="empty-content">
|
|
<i class="fas fa-file-alt fa-3x"></i>
|
|
<h2>暂无文档</h2>
|
|
<p>系统中还没有添加任何使用教程文档。</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// 创建文档列表
|
|
let html = '<h2>文档目录</h2><ul class="doc-list">';
|
|
data.forEach((doc, index) => {
|
|
// 确保doc有效
|
|
if (doc && doc.id && doc.title) {
|
|
html += `
|
|
<li class="doc-item" data-id="${doc.id}">
|
|
<a href="javascript:void(0)" onclick="showDocument(${index})">
|
|
<i class="fas fa-file-alt"></i>
|
|
<span>${doc.title}</span>
|
|
</a>
|
|
</li>
|
|
`;
|
|
} else {
|
|
console.warn('发现无效的文档数据:', doc);
|
|
}
|
|
});
|
|
html += '</ul>';
|
|
|
|
docListContainer.innerHTML = html;
|
|
|
|
// 默认加载第一篇文档
|
|
if (data.length > 0 && data[0]) {
|
|
showDocument(0);
|
|
// 激活第一个列表项
|
|
const firstLink = docListContainer.querySelector('.doc-item a');
|
|
if (firstLink) {
|
|
firstLink.classList.add('active');
|
|
}
|
|
} else {
|
|
// 如果第一个文档无效,显示空状态
|
|
docContentContainer.innerHTML = `
|
|
<div class="empty-content">
|
|
<i class="fas fa-file-alt fa-3x"></i>
|
|
<p>请从左侧选择一篇文档查看</p>
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (error) {
|
|
console.error('加载文档列表失败:', error);
|
|
documentationLoaded = false; // 加载失败,允许重试
|
|
|
|
if (docListContainer) {
|
|
docListContainer.innerHTML = `
|
|
<h2>文档目录</h2>
|
|
<div class="error-item">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<p>${error.message}</p>
|
|
<button class="btn btn-sm btn-primary mt-2" onclick="loadAndDisplayDocumentation()">重试</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (docContentContainer) {
|
|
docContentContainer.innerHTML = `
|
|
<div class="error-container">
|
|
<i class="fas fa-exclamation-triangle fa-3x"></i>
|
|
<h2>加载失败</h2>
|
|
<p>无法获取文档列表: ${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
// ========================================
|
|
// === 文档加载相关函数结束 ===
|
|
// ========================================
|
|
|
|
// ========================================
|
|
// === 全局变量和状态 ===
|
|
// ========================================
|
|
let proxyDomain = '';
|
|
let currentIndex = 0;
|
|
let items = [];
|
|
let currentPage = 1;
|
|
let currentSearchTerm = '';
|
|
let totalPages = 1;
|
|
let currentTagPage = 1;
|
|
let currentImageData = null;
|
|
|
|
// 初始化时加载代理域名配置
|
|
async function initProxyDomain() {
|
|
try {
|
|
const response = await fetch('/api/config');
|
|
if (response.ok) {
|
|
const config = await response.json();
|
|
if (config.proxyDomain) {
|
|
proxyDomain = config.proxyDomain;
|
|
console.log('成功加载代理域名:', proxyDomain);
|
|
} else {
|
|
console.warn('配置中没有proxyDomain字段');
|
|
proxyDomain = 'registry-1.docker.io'; // 使用默认值
|
|
}
|
|
} else {
|
|
console.error('加载配置失败:', response.status, response.statusText);
|
|
proxyDomain = 'registry-1.docker.io'; // 使用默认值
|
|
}
|
|
} catch (error) {
|
|
console.error('初始化代理域名失败:', error);
|
|
proxyDomain = 'registry-1.docker.io'; // 使用默认值
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// === 新增:全局提示函数 ===
|
|
// ========================================
|
|
function showToastNotification(message, type = 'info') { // types: info, success, error
|
|
// 移除任何现有的通知
|
|
const existingNotification = document.querySelector('.toast-notification');
|
|
if (existingNotification) {
|
|
existingNotification.remove();
|
|
}
|
|
|
|
// 创建新的通知元素
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast-notification ${type}`;
|
|
|
|
// 设置图标和内容
|
|
let iconClass = 'fas fa-info-circle';
|
|
if (type === 'success') iconClass = 'fas fa-check-circle';
|
|
if (type === 'error') iconClass = 'fas fa-exclamation-circle';
|
|
|
|
toast.innerHTML = `<i class="${iconClass}"></i> ${message}`;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
// 动画效果 (如果需要的话,可以在CSS中定义 @keyframes fadeIn)
|
|
// toast.style.animation = 'fadeIn 0.3s ease-in';
|
|
|
|
// 设定时间后自动移除
|
|
setTimeout(() => {
|
|
toast.style.opacity = '0'; // 开始淡出
|
|
toast.style.transition = 'opacity 0.3s ease-out';
|
|
setTimeout(() => toast.remove(), 300); // 淡出后移除DOM
|
|
}, 3500); // 显示 3.5 秒
|
|
}
|
|
|
|
// ========================================
|
|
// === 其他函数定义 ===
|
|
// ========================================
|
|
// 标签切换功能
|
|
function switchTab(tabName) {
|
|
const tabs = document.querySelectorAll('.tab');
|
|
const contents = document.querySelectorAll('.content');
|
|
const features = document.querySelector('#searchContent .features');
|
|
tabs.forEach(tab => tab.classList.remove('active'));
|
|
contents.forEach(content => content.classList.remove('active'));
|
|
// 更新为支持3个选项卡
|
|
let tabIndex = 1;
|
|
if (tabName === 'search') {
|
|
tabIndex = 2;
|
|
// 只有在没有搜索结果时显示底部特性说明
|
|
const searchResults = document.getElementById('searchResults');
|
|
if (!searchResults.innerHTML.trim()) {
|
|
features.style.display = 'grid';
|
|
}
|
|
} else if (tabName === 'documentation') {
|
|
tabIndex = 3;
|
|
}
|
|
|
|
document.querySelector(`.tab:nth-child(${tabIndex})`).classList.add('active');
|
|
document.getElementById(`${tabName}Content`).classList.add('active');
|
|
// 重置显示
|
|
if (document.getElementById('searchResultsContainer')) {
|
|
document.getElementById('searchResultsContainer').style.display = 'block';
|
|
}
|
|
if (document.getElementById('searchResultsList')) {
|
|
document.getElementById('searchResultsList').style.display = 'block';
|
|
}
|
|
if (document.getElementById('imageTagsView')) {
|
|
document.getElementById('imageTagsView').style.display = 'none';
|
|
}
|
|
document.getElementById('result').style.display = 'none';
|
|
document.getElementById('searchResults').style.display = 'none';
|
|
document.getElementById('paginationContainer').style.display = 'none';
|
|
|
|
if (tabName === 'documentation') {
|
|
loadAndDisplayDocumentation();
|
|
} else if (tabName === 'accelerate') {
|
|
// 重置显示状态
|
|
document.querySelector('.quick-guide').style.display = 'block';
|
|
document.querySelector('.popular-images').style.display = 'block';
|
|
document.getElementById('result').style.display = 'none';
|
|
|
|
const imageInput = document.getElementById('imageInput').value.trim();
|
|
if (imageInput) {
|
|
generateCommands(imageInput);
|
|
}
|
|
document.getElementById('searchInput').value = '';
|
|
document.getElementById('searchResults').innerHTML = '';
|
|
document.querySelector('.popular-images').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// 添加formatNumber函数定义
|
|
function formatNumber(num) {
|
|
if (num >= 1000000000) {
|
|
return (num >= 1500000000 ? '1B+' : '1B');
|
|
} else if (num >= 1000000) {
|
|
const m = Math.floor(num / 1000000);
|
|
return (m >= 100 ? '100M+' : m + 'M');
|
|
} else if (num >= 1000) {
|
|
const k = Math.floor(num / 1000);
|
|
return (k >= 100 ? '100K+' : k + 'K');
|
|
}
|
|
return num.toString();
|
|
}
|
|
|
|
// 生成加速命令
|
|
function generateCommands(imageInput) {
|
|
if (!imageInput) {
|
|
imageInput = document.getElementById('imageInput').value.trim();
|
|
}
|
|
if (!imageInput) {
|
|
alert('请输入 Docker 镜像名称');
|
|
return;
|
|
}
|
|
let [imageName, tag] = imageInput.split(':');
|
|
tag = tag || 'latest';
|
|
|
|
let originalImage = `${imageName}:${tag}`;
|
|
let proxyImage = '';
|
|
if (!imageName.includes('/')) {
|
|
proxyImage = `${proxyDomain}/library/${imageName}:${tag}`;
|
|
} else {
|
|
proxyImage = `${proxyDomain}/${imageName}:${tag}`;
|
|
}
|
|
|
|
const commands = [
|
|
{ title: "代理拉取镜像", cmd: `docker pull ${proxyImage}` },
|
|
{ title: "原始拉取命令", cmd: `docker pull ${originalImage}` },
|
|
{ title: "重命名镜像", cmd: `docker tag ${proxyImage} ${originalImage}` },
|
|
{ title: "删除代理镜像", cmd: `docker rmi ${proxyImage}` }
|
|
];
|
|
|
|
const resultDiv = document.getElementById('result');
|
|
const container = document.getElementById('commandsContainer');
|
|
container.innerHTML = '';
|
|
|
|
// 将生成的命令添加到结果容器中
|
|
commands.forEach((command, index) => {
|
|
const cmdDiv = document.createElement('div');
|
|
cmdDiv.className = 'step';
|
|
cmdDiv.innerHTML = `
|
|
<h3>${index + 1}. ${command.title}</h3>
|
|
<div class="command-terminal">
|
|
<div class="terminal-header">
|
|
<div class="terminal-button button-red"></div>
|
|
<div class="terminal-button button-yellow"></div>
|
|
<div class="terminal-button button-green"></div>
|
|
</div>
|
|
<pre><code>${command.cmd}</code>
|
|
<button class="copy-btn" onclick="copyToClipboard('${command.cmd}', this)">复制</button>
|
|
</pre>
|
|
</div>
|
|
`;
|
|
container.appendChild(cmdDiv);
|
|
});
|
|
|
|
// 显示结果并隐藏其他内容
|
|
resultDiv.style.display = 'flex';
|
|
resultDiv.style.flexDirection = 'column';
|
|
document.querySelector('.quick-guide').style.display = 'none';
|
|
document.querySelector('.features').style.display = 'none';
|
|
}
|
|
|
|
// 复制命令到剪贴板
|
|
function copyToClipboard(text, element) {
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
showNotification();
|
|
}, (err) => {
|
|
console.error('无法复制文本: ', err);
|
|
});
|
|
} else {
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = text;
|
|
document.body.appendChild(textarea);
|
|
textarea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textarea);
|
|
showNotification();
|
|
}
|
|
}
|
|
|
|
function showNotification() {
|
|
// 移除任何现有的通知
|
|
const existingNotification = document.querySelector('.copy-success');
|
|
if (existingNotification) {
|
|
existingNotification.remove();
|
|
}
|
|
// 创建新的通知
|
|
const successDiv = document.createElement('div');
|
|
successDiv.className = 'copy-success';
|
|
successDiv.textContent = '已复制到剪贴板';
|
|
// 添加到body而不是pre元素
|
|
document.body.appendChild(successDiv);
|
|
// 1.5秒后移除提示
|
|
setTimeout(() => {
|
|
successDiv.remove();
|
|
}, 1500);
|
|
}
|
|
|
|
// 改进的API请求函数,支持自动重试
|
|
async function fetchWithRetry(url, options = {}, retries = 3, retryDelay = 1000) {
|
|
try {
|
|
const response = await fetch(url, options);
|
|
|
|
// 检查响应状态
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
// 检查内容类型
|
|
const contentType = response.headers.get("content-type");
|
|
if (!contentType || !contentType.includes("application/json")) {
|
|
throw new Error('服务器返回了非JSON格式的数据,请联系管理员');
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
// 如果没有剩余重试次数,抛出异常
|
|
if (retries <= 0) throw error;
|
|
|
|
console.warn(`请求失败,将在${retryDelay}ms后重试 (剩余${retries}次): ${error.message}`);
|
|
|
|
// 等待重试延迟
|
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
|
|
// 递归重试,增加延迟时间
|
|
return fetchWithRetry(url, options, retries - 1, retryDelay * 1.5);
|
|
}
|
|
}
|
|
|
|
// 搜索功能 - 支持分页
|
|
async function searchDockerHub(page = 1) {
|
|
const searchTerm = document.getElementById('searchInput').value.trim();
|
|
if (!searchTerm) {
|
|
showToastNotification('请输入搜索关键词', 'info');
|
|
return;
|
|
}
|
|
// 如果搜索词改变,重置为第1页
|
|
if (currentSearchTerm !== searchTerm) {
|
|
page = 1;
|
|
currentSearchTerm = searchTerm;
|
|
}
|
|
|
|
currentPage = page;
|
|
const searchResults = document.getElementById('searchResults');
|
|
searchResults.innerHTML = '<div class="loading-indicator">正在搜索...</div>';
|
|
searchResults.style.display = 'block'; // 确保搜索结果可见
|
|
// 隐藏底部特性说明
|
|
const features = document.querySelector('#searchContent .features');
|
|
features.style.display = 'none';
|
|
|
|
// 当执行搜索时,确保返回到搜索结果列表视图
|
|
document.getElementById('searchResultsList').style.display = 'block';
|
|
document.getElementById('imageTagsView').style.display = 'none';
|
|
|
|
try {
|
|
console.log(`搜索Docker Hub: 关键词=${searchTerm}, 页码=${page}`);
|
|
|
|
// 使用新的fetchWithRetry函数
|
|
const data = await fetchWithRetry(
|
|
`/api/dockerhub/search?term=${encodeURIComponent(searchTerm)}&page=${page}`
|
|
);
|
|
|
|
const results = data.results;
|
|
const officialImages = results.filter(result => result.is_official);
|
|
const unofficialImages = results.filter(result => !result.is_official)
|
|
.sort((a, b) => (b.star_count || 0) - (a.star_count || 0));
|
|
const totalCount = data.count || 0;
|
|
totalPages = Math.ceil(totalCount / 25);
|
|
|
|
if (data.results && data.results.length > 0) {
|
|
searchResults.innerHTML = '';
|
|
officialImages.forEach(result => {
|
|
searchResults.appendChild(createResultItem(result, true));
|
|
});
|
|
unofficialImages.forEach(result => {
|
|
searchResults.appendChild(createResultItem(result, false));
|
|
});
|
|
|
|
updatePagination(page, totalPages);
|
|
document.getElementById('paginationContainer').style.display = 'flex';
|
|
|
|
} else {
|
|
searchResults.innerHTML = '<div class="empty-result"><i class="fas fa-search"></i><p>未找到匹配的镜像</p></div>';
|
|
document.getElementById('paginationContainer').style.display = 'none';
|
|
}
|
|
} catch (error) {
|
|
console.error('搜索出错:', error);
|
|
searchResults.innerHTML = `
|
|
<div class="error-message">
|
|
<i class="fas fa-exclamation-circle"></i>
|
|
<p>搜索时发生错误: ${error.message}</p>
|
|
<button onclick="searchDockerHub(${page})" class="retry-btn">
|
|
<i class="fas fa-redo"></i> 重试
|
|
</button>
|
|
</div>`;
|
|
document.getElementById('paginationContainer').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// 更新分页控件
|
|
function updatePagination(currentPage, totalPages) {
|
|
const paginationContainer = document.getElementById('paginationContainer');
|
|
const prevBtn = document.getElementById('prevPageBtn');
|
|
const nextBtn = document.getElementById('nextPageBtn');
|
|
const pageInfo = document.getElementById('pageInfo');
|
|
|
|
// 显示分页控件
|
|
paginationContainer.style.display = 'flex';
|
|
// 更新页码信息
|
|
pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
|
|
|
|
// 根据当前页码禁用或启用上一页/下一页按钮
|
|
prevBtn.disabled = currentPage <= 1;
|
|
nextBtn.disabled = currentPage >= totalPages;
|
|
}
|
|
|
|
// 更新TAG分页控件
|
|
function updateTagPagination(currentPage, totalPages) {
|
|
const paginationContainer = document.getElementById('tagPaginationContainer');
|
|
const prevBtn = document.getElementById('tagPrevPageBtn');
|
|
const nextBtn = document.getElementById('tagNextPageBtn');
|
|
const pageInfo = document.getElementById('tagPageInfo');
|
|
|
|
// 显示分页控件
|
|
paginationContainer.style.display = 'flex';
|
|
// 更新页码信息
|
|
pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
|
|
|
|
// 根据当前页码禁用或启用上一页/下一页按钮
|
|
prevBtn.disabled = currentPage <= 1;
|
|
nextBtn.disabled = currentPage >= totalPages;
|
|
}
|
|
|
|
function createResultItem(result, isOfficial) {
|
|
const resultItem = document.createElement('div');
|
|
resultItem.className = `search-result-item ${isOfficial ? 'official-image' : ''}`;
|
|
|
|
// 确保获取正确的描述字段 - 修复描述信息缺失问题
|
|
const description = result.description || result.short_description || '暂无描述';
|
|
|
|
resultItem.innerHTML = `
|
|
<div class="result-header">
|
|
<div class="title-badge">
|
|
<h3>${result.name || result.repo_name || '未知名称'}</h3>
|
|
${isOfficial ? '<span class="official-badge"><i class="fas fa-check-circle"></i> 官方</span>' : ''}
|
|
</div>
|
|
<div class="result-stats">
|
|
<span class="stats"><i class="fas fa-star"></i> ${formatNumber(result.star_count || 0)}</span>
|
|
<span class="stats"><i class="fas fa-download"></i> ${formatNumber(result.pull_count || 0)}</span>
|
|
</div>
|
|
</div>
|
|
<p class="result-description">${description}</p>
|
|
<div class="result-actions">
|
|
<button class="action-btn primary" onclick="useImage('${result.name || result.repo_name}')">
|
|
<i class="fas fa-rocket"></i> 使用此镜像
|
|
</button>
|
|
<button class="action-btn secondary" onclick="viewImageDetails('${result.name || result.repo_name}', ${isOfficial}, '${encodeURIComponent(description)}', ${result.star_count || 0}, ${result.pull_count || 0})">
|
|
<i class="fas fa-tags"></i> 查看标签
|
|
</button>
|
|
</div>
|
|
`;
|
|
return resultItem;
|
|
}
|
|
|
|
// 修改查看标签详情函数 - 改进错误处理
|
|
async function viewImageDetails(imageName, isOfficial, description, stars, pulls) {
|
|
// 保存当前镜像信息
|
|
currentImageData = {
|
|
name: imageName,
|
|
isOfficial: isOfficial,
|
|
description: decodeURIComponent(description || ''),
|
|
stars: stars,
|
|
pulls: pulls
|
|
};
|
|
|
|
// 显示加载中状态
|
|
const imageTagsView = document.getElementById('imageTagsView');
|
|
imageTagsView.innerHTML = '<div class="loading-container"><div class="loading-indicator">正在加载镜像信息...</div></div>';
|
|
document.getElementById('searchResultsList').style.display = 'none';
|
|
imageTagsView.style.display = 'block';
|
|
|
|
try {
|
|
// 使用新的fetchWithRetry函数获取标签计数
|
|
const countApiUrl = `/api/dockerhub/tag-count?name=${encodeURIComponent(currentImageData.name)}&official=${currentImageData.isOfficial}`;
|
|
console.log('Requesting tag count from:', countApiUrl);
|
|
|
|
const countData = await fetchWithRetry(countApiUrl);
|
|
console.log('Received tag count data:', countData);
|
|
|
|
const tagCount = countData.count || 0;
|
|
const recommendedMode = countData.recommended_mode || 'paginated';
|
|
|
|
// 根据标签数量判断是否显示警告
|
|
let warningMessage = '';
|
|
let loadAllBtnDisabled = false;
|
|
|
|
if (tagCount > 1000) {
|
|
warningMessage = `<div class="tag-count-warning">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<p>该镜像包含 <strong>${tagCount}</strong> 个标签,加载全部可能会很慢。建议使用分页浏览或利用搜索功能查找特定标签。</p>
|
|
</div>`;
|
|
loadAllBtnDisabled = true;
|
|
} else if (tagCount > 500) {
|
|
warningMessage = `<div class="tag-count-warning moderate">
|
|
<i class="fas fa-info-circle"></i>
|
|
<p>该镜像包含 <strong>${tagCount}</strong> 个标签,加载全部可能需要一些时间。</p>
|
|
</div>`;
|
|
}
|
|
|
|
// 重新构建标签视图内容
|
|
imageTagsView.innerHTML = `
|
|
<div class="tag-header">
|
|
<div class="tag-breadcrumb">
|
|
<a href="javascript:void(0);" onclick="showSearchResults()">返回搜索结果</a>
|
|
</div>
|
|
<h2 id="currentImageTitle">${imageName}</h2>
|
|
<p id="imageDescription" class="image-description">${currentImageData.description || '暂无描述'}</p>
|
|
<div class="image-meta">
|
|
<span id="imageStars"><i class="fas fa-star"></i> ${formatNumber(currentImageData.stars || 0)} 星标</span>
|
|
<span id="imagePulls"><i class="fas fa-download"></i> ${formatNumber(currentImageData.pulls || 0)} 下载</span>
|
|
<span id="imageTags"><i class="fas fa-tags"></i> ${formatNumber(tagCount)} 个标签</span>
|
|
</div>
|
|
</div>
|
|
|
|
${warningMessage}
|
|
|
|
<div class="tag-actions">
|
|
<div class="tag-search-container">
|
|
<input type="text" id="tagSearchInput" placeholder="搜索TAG..." onkeyup="filterTags()">
|
|
</div>
|
|
<button id="loadAllTagsBtn" class="load-all-btn" onclick="loadAllTags()" ${loadAllBtnDisabled ? 'disabled' : ''}>
|
|
<i class="fas fa-cloud-download-alt"></i> 加载全部TAG
|
|
</button>
|
|
</div>
|
|
|
|
<div id="tagsResults"></div>
|
|
|
|
<div class="pagination-container" id="tagPaginationContainer" style="display: none;">
|
|
<button id="tagPrevPageBtn" onclick="loadImageTags(currentTagPage - 1)" disabled>
|
|
<i class="fas fa-chevron-left"></i> 上一页
|
|
</button>
|
|
<span id="tagPageInfo">第 1 页</span>
|
|
<button id="tagNextPageBtn" onclick="loadImageTags(currentTagPage + 1)">
|
|
下一页 <i class="fas fa-chevron-right"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// 加载标签列表
|
|
currentTagPage = 1;
|
|
await loadImageTags(1);
|
|
enhanceTagSearchContainer();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading image details:', error);
|
|
imageTagsView.innerHTML = `
|
|
<div class="tag-header">
|
|
<div class="tag-breadcrumb">
|
|
<a href="javascript:void(0);" onclick="showSearchResults()">返回搜索结果</a>
|
|
</div>
|
|
<div class="error-message">
|
|
<i class="fas fa-exclamation-circle"></i>
|
|
<p>加载镜像详情失败: ${error.message}</p>
|
|
<button onclick="viewImageDetails('${currentImageData.name}', ${currentImageData.isOfficial}, '${encodeURIComponent(currentImageData.description)}', ${currentImageData.stars}, ${currentImageData.pulls})" class="retry-btn">
|
|
<i class="fas fa-redo"></i> 重试
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
showToastNotification(`加载镜像详情失败: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// 新增: 加载所有标签 - 改进错误处理
|
|
async function loadAllTags() {
|
|
if (!currentImageData) {
|
|
console.error('No image data available');
|
|
return;
|
|
}
|
|
|
|
const loadAllTagsBtn = document.getElementById('loadAllTagsBtn');
|
|
const tagsResults = document.getElementById('tagsResults');
|
|
|
|
// 禁用按钮,显示加载状态
|
|
loadAllTagsBtn.disabled = true;
|
|
loadAllTagsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在加载全部TAG...';
|
|
tagsResults.innerHTML = '<div class="loading-indicator">加载所有TAG中,这可能需要一些时间...</div>';
|
|
|
|
try {
|
|
// 先获取标签总数
|
|
const countApiUrl = `/api/dockerhub/tag-count?name=${encodeURIComponent(currentImageData.name)}&official=${currentImageData.isOfficial}`;
|
|
const countData = await fetchWithRetry(countApiUrl);
|
|
|
|
const totalTags = countData.count || 0;
|
|
|
|
if (totalTags === 0) {
|
|
tagsResults.innerHTML = '<div class="message-container"><i class="fas fa-info-circle"></i><p>未找到标签信息</p></div>';
|
|
showToastNotification(`该镜像没有可用的标签`, 'info');
|
|
loadAllTagsBtn.disabled = false;
|
|
loadAllTagsBtn.innerHTML = '<i class="fas fa-cloud-download-alt"></i> 加载全部TAG';
|
|
return;
|
|
}
|
|
|
|
// 计算需要请求的次数 (每页最多100个标签)
|
|
const pageSize = 100;
|
|
const totalPages = Math.ceil(totalTags / pageSize);
|
|
|
|
// 如果标签太多,提示用户
|
|
if (totalTags > 3000) {
|
|
const confirmLoad = confirm(`该镜像包含 ${totalTags} 个标签,加载全部可能会很慢。确定继续吗?`);
|
|
if (!confirmLoad) {
|
|
loadAllTagsBtn.disabled = false;
|
|
loadAllTagsBtn.innerHTML = '<i class="fas fa-cloud-download-alt"></i> 加载全部TAG';
|
|
tagsResults.innerHTML = '';
|
|
await loadImageTags(1); // 加载第一页
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 所有标签的集合
|
|
let allTags = [];
|
|
let loadedPages = 0;
|
|
|
|
// 更新加载进度的函数
|
|
const updateProgress = () => {
|
|
loadAllTagsBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> 正在加载 (${Math.round((loadedPages/totalPages)*100)}%)`;
|
|
tagsResults.innerHTML = `<div class="loading-indicator">已加载 ${allTags.length} / ${totalTags} 个标签 (${Math.round((loadedPages/totalPages)*100)}%)...</div>`;
|
|
};
|
|
|
|
// 分批加载所有标签
|
|
for (let page = 1; page <= totalPages; page++) {
|
|
try {
|
|
const apiUrl = `/api/dockerhub/tags?name=${encodeURIComponent(currentImageData.name)}&official=${currentImageData.isOfficial}&page=${page}&page_size=${pageSize}`;
|
|
|
|
// 使用新的fetchWithRetry函数
|
|
const data = await fetchWithRetry(apiUrl);
|
|
|
|
if (data.results && Array.isArray(data.results)) {
|
|
// 处理标签中缺少平台信息的情况
|
|
const processedTags = data.results.map(tag => {
|
|
if (!tag.images || !Array.isArray(tag.images) || tag.images.length === 0) {
|
|
tag.images = [];
|
|
}
|
|
return tag;
|
|
});
|
|
|
|
allTags = allTags.concat(processedTags);
|
|
}
|
|
|
|
loadedPages++;
|
|
updateProgress();
|
|
|
|
} catch (error) {
|
|
console.error(`加载第 ${page} 页标签出错:`, error);
|
|
}
|
|
}
|
|
|
|
if (allTags.length > 0) {
|
|
// 为加载的所有标签实现客户端分页
|
|
window.allLoadedTags = allTags; // 保存所有标签到全局变量
|
|
window.currentAllTagsPage = 1;
|
|
window.tagsPerPage = 25; // 修改: 每页显示25个标签而不是50个
|
|
|
|
// 计算总页数
|
|
const clientTotalPages = Math.ceil(allTags.length / window.tagsPerPage);
|
|
|
|
// 显示第一页标签(这会自动创建分页控制器)
|
|
displayAllTagsPage(1);
|
|
|
|
showToastNotification(`成功加载 ${allTags.length} / ${totalTags} 个标签,分${clientTotalPages}页显示`, 'success');
|
|
|
|
// 滚动到顶部
|
|
window.scrollTo({
|
|
top: document.getElementById('imageTagsView').offsetTop - 80,
|
|
behavior: 'smooth'
|
|
});
|
|
|
|
} else {
|
|
tagsResults.innerHTML = '<div class="message-container"><i class="fas fa-info-circle"></i><p>未找到标签信息</p></div>';
|
|
showToastNotification(`未能加载标签`, 'info');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('加载全部标签失败:', error);
|
|
tagsResults.innerHTML = `
|
|
<div class="error-message">
|
|
<i class="fas fa-exclamation-circle"></i>
|
|
<p>加载全部标签失败: ${error.message}</p>
|
|
<button onclick="loadImageTags(1)" class="retry-btn">
|
|
<i class="fas fa-redo"></i> 返回常规模式
|
|
</button>
|
|
</div>
|
|
`;
|
|
showToastNotification(`加载全部标签失败: ${error.message}`, 'error');
|
|
} finally {
|
|
// 恢复按钮状态
|
|
loadAllTagsBtn.disabled = false;
|
|
loadAllTagsBtn.innerHTML = '<i class="fas fa-cloud-download-alt"></i> 加载全部TAG';
|
|
}
|
|
}
|
|
|
|
// 添加 loadImageTags 函数定义
|
|
async function loadImageTags(page = 1) {
|
|
if (!currentImageData) {
|
|
console.error('No image data available');
|
|
return;
|
|
}
|
|
const tagsResults = document.getElementById('tagsResults');
|
|
tagsResults.innerHTML = '<div class="loading-indicator">加载TAG列表中...</div>';
|
|
|
|
try {
|
|
// 构建API URL
|
|
const apiUrl = `/api/dockerhub/tags?name=${encodeURIComponent(currentImageData.name)}&official=${currentImageData.isOfficial}&page=${page}&page_size=25`;
|
|
console.log('Requesting tags from:', apiUrl);
|
|
|
|
// 使用fetchWithRetry获取数据
|
|
const data = await fetchWithRetry(apiUrl);
|
|
console.log('Received tags data:', data);
|
|
currentTagPage = page; // 更新当前页码
|
|
|
|
if (data.results && data.results.length > 0) {
|
|
// 处理标签中缺少平台信息的情况
|
|
const processedTags = data.results.map(tag => {
|
|
// 确保tag.images存在
|
|
if (!tag.images || !Array.isArray(tag.images) || tag.images.length === 0) {
|
|
tag.images = [];
|
|
}
|
|
return tag;
|
|
});
|
|
|
|
// 显示标签列表
|
|
displayTags(processedTags);
|
|
|
|
// 更新分页信息
|
|
updateTagPagination(page, Math.ceil((data.count || 0) / 25));
|
|
document.getElementById('tagPaginationContainer').style.display = 'flex';
|
|
|
|
// 更新页面显示信息
|
|
const tagStatsDiv = document.querySelector('.tag-search-stats');
|
|
if (tagStatsDiv) {
|
|
tagStatsDiv.innerHTML = `<p>共找到 <strong>${data.count || processedTags.length}</strong> 个标签,当前显示第 <strong>${(page-1)*25+1}</strong> 至 <strong>${Math.min(page*25, data.count)}</strong> 个</p>`;
|
|
}
|
|
|
|
} else {
|
|
tagsResults.innerHTML = '<div class="message-container"><i class="fas fa-info-circle"></i><p>未找到标签信息</p></div>';
|
|
document.getElementById('tagPaginationContainer').style.display = 'none';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading tags:', error);
|
|
tagsResults.innerHTML = `
|
|
<div class="error-message">
|
|
<i class="fas fa-exclamation-circle"></i>
|
|
<p>加载标签失败: ${error.message}</p>
|
|
<button onclick="loadImageTags(${page})" class="retry-btn">
|
|
<i class="fas fa-redo"></i> 重试
|
|
</button>
|
|
</div>
|
|
`;
|
|
document.getElementById('tagPaginationContainer').style.display = 'none';
|
|
|
|
showToastNotification(`加载标签失败: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// 新增: 显示客户端分页控制器
|
|
function displayClientPagination(totalPages) {
|
|
const tagsResults = document.getElementById('tagsResults');
|
|
|
|
// 创建分页容器
|
|
const paginationDiv = document.createElement('div');
|
|
paginationDiv.className = 'pagination-container'; // 使用相同的样式类名
|
|
paginationDiv.id = 'clientPaginationContainer';
|
|
|
|
// 添加分页控制,格式与默认分页控制器相同
|
|
paginationDiv.innerHTML = `
|
|
<button id="clientPrevPageBtn" onclick="navigateAllTagsPage(-1)" disabled>
|
|
<i class="fas fa-chevron-left"></i> 上一页
|
|
</button>
|
|
<span id="clientPageInfo">第 1 页 / 共 ${totalPages} 页</span>
|
|
<button id="clientNextPageBtn" onclick="navigateAllTagsPage(1)" ${totalPages <= 1 ? 'disabled' : ''}>
|
|
下一页 <i class="fas fa-chevron-right"></i>
|
|
</button>
|
|
`;
|
|
|
|
// 确保分页控制器添加到表格底部
|
|
const existingPagination = document.getElementById('tagPaginationContainer');
|
|
if (existingPagination && existingPagination.parentNode) {
|
|
// 在原始分页控制器的位置插入新的分页控制器
|
|
existingPagination.parentNode.insertBefore(paginationDiv, existingPagination);
|
|
// 隐藏原来的分页控件
|
|
existingPagination.style.display = 'none';
|
|
} else {
|
|
// 如果找不到原始分页控制器,添加到结果容器末尾
|
|
tagsResults.appendChild(paginationDiv);
|
|
}
|
|
}
|
|
|
|
// 新增: 切换到指定页面
|
|
function displayAllTagsPage(page) {
|
|
if (!window.allLoadedTags) return;
|
|
|
|
const totalTags = window.allLoadedTags.length;
|
|
// 修改: 将每页标签数量从50改为25
|
|
window.tagsPerPage = 25; // 每页显示25个标签
|
|
const tagsPerPage = window.tagsPerPage;
|
|
const totalPages = Math.ceil(totalTags / tagsPerPage);
|
|
|
|
// 确保页码在有效范围内
|
|
if (page < 1) page = 1;
|
|
if (page > totalPages) page = totalPages;
|
|
|
|
window.currentAllTagsPage = page;
|
|
|
|
// 计算当前页的标签
|
|
const startIndex = (page - 1) * tagsPerPage;
|
|
const endIndex = Math.min(startIndex + tagsPerPage, totalTags);
|
|
const currentPageTags = window.allLoadedTags.slice(startIndex, endIndex);
|
|
|
|
// 使用现有的displayTags函数显示当前页的标签
|
|
displayTags(currentPageTags);
|
|
enhanceTagSearchContainer();
|
|
|
|
// 更新分页信息
|
|
const pageInfo = document.getElementById('clientPageInfo');
|
|
if (pageInfo) {
|
|
pageInfo.textContent = `第 ${page} 页 / 共 ${totalPages} 页`;
|
|
}
|
|
|
|
// 更新按钮状态
|
|
const prevBtn = document.getElementById('clientPrevPageBtn');
|
|
const nextBtn = document.getElementById('clientNextPageBtn');
|
|
if (prevBtn) prevBtn.disabled = page <= 1;
|
|
if (nextBtn) nextBtn.disabled = page >= totalPages;
|
|
|
|
// 更新标签统计信息
|
|
const tagStatsDiv = document.querySelector('.tag-search-stats');
|
|
if (tagStatsDiv) {
|
|
tagStatsDiv.innerHTML = `<p>显示 <strong>${startIndex + 1}-${endIndex}</strong> 个标签,共 <strong>${totalTags}</strong> 个</p>`;
|
|
}
|
|
|
|
// 创建新的客户端分页控制器
|
|
const clientPaginationContainer = document.getElementById('clientPaginationContainer');
|
|
if (!clientPaginationContainer) {
|
|
displayClientPagination(totalPages);
|
|
}
|
|
}
|
|
|
|
// 新增: 页面导航函数
|
|
function navigateAllTagsPage(direction) {
|
|
const newPage = window.currentAllTagsPage + direction;
|
|
displayAllTagsPage(newPage);
|
|
|
|
// 滚动到分页控制器位置,确保用户可以看到分页器
|
|
const paginationContainer = document.getElementById('clientPaginationContainer');
|
|
if (paginationContainer) {
|
|
paginationContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}
|
|
|
|
// 显示TAG列表 - 改进默认排序和显示
|
|
function displayTags(tags) {
|
|
const tagsResults = document.getElementById('tagsResults');
|
|
tagsResults.innerHTML = '';
|
|
|
|
if (tags.length === 0) {
|
|
tagsResults.innerHTML = '<div class="message-container">没有找到匹配的TAG</div>';
|
|
return;
|
|
}
|
|
|
|
// 添加标签搜索统计信息
|
|
const searchStatsDiv = document.createElement('div');
|
|
searchStatsDiv.className = 'tag-search-stats';
|
|
searchStatsDiv.innerHTML = `<p>共找到 <strong>${tags.length}</strong> 个标签</p>`;
|
|
tagsResults.appendChild(searchStatsDiv);
|
|
|
|
// 添加标签排序功能
|
|
const sortContainer = document.createElement('div');
|
|
sortContainer.className = 'tag-sort-container';
|
|
sortContainer.innerHTML = `
|
|
<label for="tagSort">排序方式:</label>
|
|
<select id="tagSort" onchange="sortTags()">
|
|
<option value="name-asc">TAG名称 (A-Z)</option>
|
|
<option value="name-desc">TAG名称 (Z-A)</option>
|
|
<option value="date-desc" selected>最新更新</option>
|
|
<option value="date-asc">最早更新</option>
|
|
<option value="size-desc">大小 (大-小)</option>
|
|
<option value="size-asc">大小 (小-大)</option>
|
|
</select>
|
|
`;
|
|
tagsResults.appendChild(sortContainer);
|
|
|
|
// 创建表格容器以启用水平滚动
|
|
const tableContainer = document.createElement('div');
|
|
tableContainer.className = 'tag-table-container';
|
|
tagsResults.appendChild(tableContainer);
|
|
|
|
const tagTable = document.createElement('table');
|
|
tagTable.className = 'tag-table';
|
|
tagTable.id = 'tagTable';
|
|
|
|
const thead = document.createElement('thead');
|
|
thead.innerHTML = `
|
|
<tr>
|
|
<th width="18%">TAG</th>
|
|
<th width="42%">OS/ARCH</th>
|
|
<th width="15%">大小</th>
|
|
<th width="15%">更新时间</th>
|
|
<th width="10%">操作</th>
|
|
</tr>
|
|
`;
|
|
tagTable.appendChild(thead);
|
|
|
|
const tbody = document.createElement('tbody');
|
|
tbody.id = 'tagTableBody';
|
|
|
|
// 使用最新更新的默认排序
|
|
window.currentTags = [...tags];
|
|
sortTagsByDate('desc');
|
|
|
|
renderTagRows(window.currentTags, tbody);
|
|
|
|
tagTable.appendChild(tbody);
|
|
tableContainer.appendChild(tagTable); // 将表格添加到容器中
|
|
|
|
// 添加调试信息
|
|
console.log(`显示了 ${tags.length} 个标签`);
|
|
}
|
|
|
|
// 新增的排序标签函数
|
|
function sortTags() {
|
|
const sortSelect = document.getElementById('tagSort');
|
|
const [sortBy, direction] = sortSelect.value.split('-');
|
|
|
|
if (sortBy === 'name') {
|
|
sortTagsByName(direction);
|
|
} else if (sortBy === 'date') {
|
|
sortTagsByDate(direction);
|
|
} else if (sortBy === 'size') {
|
|
sortTagsBySize(direction);
|
|
}
|
|
|
|
const tbody = document.getElementById('tagTableBody');
|
|
tbody.innerHTML = '';
|
|
renderTagRows(window.currentTags, tbody);
|
|
}
|
|
|
|
// 按名称排序
|
|
function sortTagsByName(direction) {
|
|
window.currentTags.sort((a, b) => {
|
|
return direction === 'asc'
|
|
? a.name.localeCompare(b.name)
|
|
: b.name.localeCompare(a.name);
|
|
});
|
|
}
|
|
|
|
// 按日期排序
|
|
function sortTagsByDate(direction) {
|
|
window.currentTags.sort((a, b) => {
|
|
const dateA = a.last_updated ? new Date(a.last_updated) : new Date(0);
|
|
const dateB = b.last_updated ? new Date(b.last_updated) : new Date(0);
|
|
return direction === 'asc' ? dateA - dateB : dateB - dateA;
|
|
});
|
|
}
|
|
|
|
// 按大小排序
|
|
function sortTagsBySize(direction) {
|
|
window.currentTags.sort((a, b) => {
|
|
const sizeA = a.full_size || 0;
|
|
const sizeB = b.full_size || 0;
|
|
return direction === 'asc' ? sizeA - sizeB : sizeB - sizeA;
|
|
});
|
|
}
|
|
|
|
// 渲染标签行
|
|
function renderTagRows(tags, tbody) {
|
|
tags.forEach((tag, index) => {
|
|
const tr = document.createElement('tr');
|
|
|
|
// 计算大小
|
|
let size = '未知';
|
|
if (tag.full_size) {
|
|
const sizeInMB = Math.round(tag.full_size / 1024 / 1024);
|
|
size = `${sizeInMB} MB`;
|
|
}
|
|
|
|
// 格式化日期
|
|
let lastUpdated = '未知';
|
|
if (tag.last_updated) {
|
|
const date = new Date(tag.last_updated);
|
|
lastUpdated = date.toLocaleDateString('zh-CN');
|
|
}
|
|
|
|
tr.innerHTML = `
|
|
<td>${tag.name}</td>
|
|
<td>${createOsArchHtml(tag.images, index)}</td>
|
|
<td>${size}</td>
|
|
<td>${lastUpdated}</td>
|
|
<td>
|
|
<button class="primary-btn" onclick="useImage('${currentImageData.name}:${tag.name}')">
|
|
<i class="fas fa-rocket"></i> 使用
|
|
</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function createOsArchHtml(images, tagIndex) {
|
|
// 确保images是有效数据
|
|
if (!images || !Array.isArray(images) || images.length === 0) {
|
|
return '<div class="tag-os-arch"><span class="tag-os-arch-item">无平台信息</span></div>';
|
|
}
|
|
|
|
// 过滤和去重平台信息,过滤掉unknown/unknown
|
|
const uniquePlatforms = [];
|
|
const seen = new Set();
|
|
images.forEach(img => {
|
|
if (img && img.os && img.architecture) {
|
|
// 跳过unknown/unknown组合
|
|
if (img.os === 'unknown' && img.architecture === 'unknown') {
|
|
return;
|
|
}
|
|
|
|
const key = `${img.os}/${img.architecture}${img.variant ? '/' + img.variant : ''}`;
|
|
if (!seen.has(key)) {
|
|
seen.add(key);
|
|
uniquePlatforms.push(img);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (uniquePlatforms.length === 0) {
|
|
return '<div class="tag-os-arch"><span class="tag-os-arch-item">无平台信息</span></div>';
|
|
}
|
|
|
|
// 改进的显示逻辑:以列表形式显示所有平台
|
|
const mainPlatforms = uniquePlatforms.slice(0, 4); // 显示前4个
|
|
const extraPlatforms = uniquePlatforms.slice(4); // 其余隐藏
|
|
|
|
let html = '<div class="tag-os-arch">';
|
|
|
|
// 显示主要平台
|
|
mainPlatforms.forEach(img => {
|
|
html += `<span class="tag-os-arch-item">${img.os}/${img.architecture}${img.variant ? '/' + img.variant : ''}</span>`;
|
|
});
|
|
|
|
// 如果有更多平台,添加展开功能
|
|
if (extraPlatforms.length > 0) {
|
|
html += `
|
|
<span class="tag-os-arch-more" onclick="toggleOsArch(${tagIndex})">
|
|
<i class="fas fa-plus-circle"></i> 显示更多(${extraPlatforms.length})
|
|
</span>
|
|
<div id="osArch${tagIndex}" class="tag-os-arch-all">
|
|
`;
|
|
|
|
extraPlatforms.forEach(img => {
|
|
html += `<span class="tag-os-arch-item">${img.os}/${img.architecture}${img.variant ? '/' + img.variant : ''}</span>`;
|
|
});
|
|
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
function toggleOsArch(tagIndex) {
|
|
const element = document.getElementById(`osArch${tagIndex}`);
|
|
element.classList.toggle('show');
|
|
const moreBtn = element.previousElementSibling;
|
|
|
|
if (element.classList.contains('show')) {
|
|
moreBtn.innerHTML = '<i class="fas fa-minus-circle"></i> 收起';
|
|
} else {
|
|
moreBtn.innerHTML = `<i class="fas fa-plus-circle"></i> 显示更多(${element.children.length})`;
|
|
}
|
|
}
|
|
|
|
// 修改TAG过滤功能 - 支持搜索所有已加载的标签
|
|
function filterTags() {
|
|
const searchTerm = document.getElementById('tagSearchInput').value.toLowerCase().trim();
|
|
|
|
// 检查是否已加载全部标签
|
|
if (window.allLoadedTags && searchTerm) {
|
|
// 在所有加载的标签中搜索
|
|
const matchedTags = window.allLoadedTags.filter(tag =>
|
|
tag.name.toLowerCase().includes(searchTerm)
|
|
);
|
|
|
|
// 更新搜索统计信息
|
|
const searchStatsDiv = document.querySelector('.tag-search-stats');
|
|
if (searchStatsDiv) {
|
|
searchStatsDiv.innerHTML = `<p>过滤结果: 共找到 <strong>${matchedTags.length}</strong> 个匹配 "${searchTerm}" 的标签 (共${window.allLoadedTags.length}个)</p>`;
|
|
}
|
|
|
|
// 如果有匹配的标签
|
|
if (matchedTags.length > 0) {
|
|
// 显示匹配的标签
|
|
displayTags(matchedTags);
|
|
|
|
// 隐藏分页控件,显示所有匹配结果
|
|
const clientPagination = document.getElementById('clientPaginationContainer');
|
|
if (clientPagination) {
|
|
clientPagination.style.display = 'none';
|
|
}
|
|
} else {
|
|
// 无匹配结果提示
|
|
const tagsResults = document.getElementById('tagsResults');
|
|
// 保留搜索统计信息
|
|
const statsHTML = tagsResults.innerHTML.split('</div>')[0] + '</div>';
|
|
tagsResults.innerHTML = statsHTML + '<div class="no-filter-results"><p>没有匹配 "' + searchTerm + '" 的TAG</p></div>';
|
|
}
|
|
|
|
return; // 已处理全局搜索,不继续执行
|
|
}
|
|
|
|
// 原有的过滤逻辑 - 只搜索当前页面上的标签
|
|
const rows = document.querySelectorAll('.tag-table tbody tr');
|
|
if (!rows.length) return;
|
|
|
|
let visibleCount = 0;
|
|
rows.forEach(row => {
|
|
const tagName = row.querySelector('td:first-child').textContent.toLowerCase();
|
|
if (tagName.includes(searchTerm)) {
|
|
row.style.display = '';
|
|
visibleCount++;
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// 更新过滤后的统计信息
|
|
const searchStatsDiv = document.querySelector('.tag-search-stats');
|
|
if (searchStatsDiv) {
|
|
if (searchTerm) {
|
|
searchStatsDiv.innerHTML = `<p>过滤结果: 共找到 <strong>${visibleCount}</strong> 个匹配 "${searchTerm}" 的标签</p>`;
|
|
} else {
|
|
searchStatsDiv.innerHTML = `<p>共找到 <strong>${rows.length}</strong> 个标签</p>`;
|
|
}
|
|
}
|
|
|
|
// 如果没有匹配的结果,显示提示
|
|
const tagsResults = document.getElementById('tagsResults');
|
|
const noResultsEl = tagsResults.querySelector('.no-filter-results');
|
|
|
|
if (visibleCount === 0 && searchTerm) {
|
|
if (!noResultsEl) {
|
|
const message = document.createElement('div');
|
|
message.className = 'no-filter-results';
|
|
message.innerHTML = `<p>没有匹配 "${searchTerm}" 的TAG</p>`;
|
|
tagsResults.appendChild(message);
|
|
}
|
|
} else if (noResultsEl) {
|
|
noResultsEl.remove();
|
|
}
|
|
}
|
|
|
|
// 添加重置搜索功能
|
|
function resetTagSearch() {
|
|
const searchInput = document.getElementById('tagSearchInput');
|
|
if (searchInput) {
|
|
searchInput.value = '';
|
|
}
|
|
|
|
// 如果已加载全部标签,重新显示当前页
|
|
if (window.allLoadedTags) {
|
|
displayAllTagsPage(window.currentAllTagsPage || 1);
|
|
// 恢复分页控件显示
|
|
const clientPagination = document.getElementById('clientPaginationContainer');
|
|
if (clientPagination) {
|
|
clientPagination.style.display = 'flex';
|
|
}
|
|
} else {
|
|
// 否则重新加载当前标签页
|
|
loadImageTags(currentTagPage);
|
|
}
|
|
}
|
|
|
|
// 修改标签搜索容器,添加重置按钮
|
|
function enhanceTagSearchContainer() {
|
|
const container = document.querySelector('.tag-search-container');
|
|
if (container) {
|
|
// 检查是否已经增强过
|
|
if (!container.querySelector('.reset-btn')) {
|
|
// 添加重置按钮
|
|
const resetBtn = document.createElement('button');
|
|
resetBtn.className = 'reset-btn';
|
|
resetBtn.innerHTML = '<i class="fas fa-times"></i> 重置';
|
|
resetBtn.onclick = resetTagSearch;
|
|
container.appendChild(resetBtn);
|
|
|
|
// 修改搜索按钮点击事件
|
|
const searchBtn = container.querySelector('.search-btn');
|
|
if (searchBtn) {
|
|
searchBtn.onclick = filterTags;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 显示指定的文档
|
|
function showDocument(index) {
|
|
console.log('显示文档索引:', index);
|
|
|
|
if (!window.documentationData || !Array.isArray(window.documentationData)) {
|
|
console.error('文档数据不可用');
|
|
return;
|
|
}
|
|
|
|
// 处理数字索引或字符串ID
|
|
let docIndex = index;
|
|
let doc = null;
|
|
|
|
if (typeof index === 'string') {
|
|
// 如果是ID,找到对应的索引
|
|
docIndex = window.documentationData.findIndex(doc =>
|
|
(doc.id === index || doc._id === index)
|
|
);
|
|
|
|
if (docIndex === -1) {
|
|
console.error('找不到ID为', index, '的文档');
|
|
return;
|
|
}
|
|
}
|
|
|
|
doc = window.documentationData[docIndex];
|
|
|
|
if (!doc) {
|
|
console.error('指定索引的文档不存在:', docIndex);
|
|
return;
|
|
}
|
|
|
|
console.log('文档数据:', doc);
|
|
|
|
// 高亮选中的文档
|
|
const docLinks = document.querySelectorAll('.doc-list li a');
|
|
docLinks.forEach((link, i) => {
|
|
if (i === docIndex) {
|
|
link.classList.add('active');
|
|
} else {
|
|
link.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
const docContent = document.getElementById('documentationText');
|
|
if (!docContent) {
|
|
console.error('找不到文档内容容器');
|
|
return;
|
|
}
|
|
|
|
// 显示加载状态
|
|
docContent.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 正在加载文档内容...</div>';
|
|
|
|
// 如果文档内容不存在,则需要获取完整内容
|
|
if (!doc.content) {
|
|
const docId = doc.id || doc._id;
|
|
console.log('获取文档内容,ID:', docId);
|
|
|
|
fetch(`/api/documentation/${docId}`)
|
|
.then(response => {
|
|
console.log('文档API响应:', response.status, response.statusText);
|
|
if (!response.ok) {
|
|
throw new Error(`获取文档内容失败: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(fullDoc => {
|
|
console.log('获取到完整文档:', fullDoc);
|
|
|
|
// 更新缓存的文档内容
|
|
window.documentationData[docIndex].content = fullDoc.content;
|
|
|
|
// 渲染文档内容
|
|
renderDocumentContent(docContent, fullDoc);
|
|
})
|
|
.catch(error => {
|
|
console.error('获取文档内容失败:', error);
|
|
docContent.innerHTML = `
|
|
<div class="error-container">
|
|
<i class="fas fa-exclamation-triangle fa-3x"></i>
|
|
<h2>加载失败</h2>
|
|
<p>无法获取文档内容: ${error.message}</p>
|
|
</div>
|
|
`;
|
|
});
|
|
} else {
|
|
// 直接渲染已有的文档内容
|
|
renderDocumentContent(docContent, doc);
|
|
}
|
|
}
|
|
|
|
// 确保showDocument函数在全局范围内可用
|
|
window.showDocument = showDocument;
|
|
|
|
// 渲染文档内容
|
|
function renderDocumentContent(container, doc) {
|
|
if (!container) return;
|
|
|
|
console.log('正在渲染文档:', doc);
|
|
|
|
// 确保有内容可渲染
|
|
if (!doc.content && !doc.path) {
|
|
container.innerHTML = `
|
|
<h1>${doc.title || '未知文档'}</h1>
|
|
<div class="empty-content">
|
|
<i class="fas fa-file-alt fa-3x"></i>
|
|
<p>该文档暂无内容</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// 根据文档内容类型进行渲染
|
|
if (doc.content) {
|
|
renderMarkdownContent(container, doc);
|
|
} else {
|
|
// 如果是文件路径但无内容,尝试获取
|
|
fetch(`/api/documentation/file?path=${encodeURIComponent(doc.id + '.md')}`)
|
|
.then(response => {
|
|
console.log('文件内容响应:', response.status, response.statusText);
|
|
if (!response.ok) {
|
|
throw new Error(`获取文件内容失败: ${response.status}`);
|
|
}
|
|
return response.text();
|
|
})
|
|
.then(content => {
|
|
console.log('获取到文件内容,长度:', content.length);
|
|
doc.content = content;
|
|
renderMarkdownContent(container, doc);
|
|
})
|
|
.catch(error => {
|
|
console.error('获取文件内容失败:', error);
|
|
container.innerHTML = `
|
|
<div class="error-container">
|
|
<i class="fas fa-exclamation-triangle fa-3x"></i>
|
|
<h2>加载失败</h2>
|
|
<p>无法获取文档内容: ${error.message}</p>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
}
|
|
|
|
// 渲染Markdown内容
|
|
function renderMarkdownContent(container, doc) {
|
|
if (!container) return;
|
|
|
|
console.log('渲染Markdown内容:', doc.title, '内容长度:', doc.content ? doc.content.length : 0);
|
|
|
|
if (doc.content) {
|
|
// 使用marked渲染Markdown内容
|
|
if (window.marked) {
|
|
try {
|
|
const parsedContent = marked.parse(doc.content);
|
|
// 结构:内容(含标题)-> 元数据
|
|
container.innerHTML = `
|
|
<div class="doc-content">${parsedContent}</div>
|
|
<div class="doc-meta">
|
|
${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
console.error('Markdown解析失败:', error);
|
|
// 发生错误时,仍然显示原始Markdown内容 + Meta
|
|
container.innerHTML = `
|
|
<div class="doc-content">${doc.content}</div>
|
|
<div class="doc-meta">
|
|
${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
} else {
|
|
// marked 不可用时,直接显示内容 + Meta
|
|
container.innerHTML = `
|
|
<div class="doc-content">${doc.content}</div>
|
|
<div class="doc-meta">
|
|
${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
} else {
|
|
// 文档无内容时,显示占位符
|
|
container.innerHTML = `
|
|
<div class="doc-content">
|
|
<div class="empty-content">
|
|
<i class="fas fa-file-alt fa-3x"></i>
|
|
<p>该文档暂无内容</p>
|
|
</div>
|
|
</div>
|
|
<div class="doc-meta">
|
|
<span>文档信息不可用</span>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 加载菜单
|
|
loadMenu();
|
|
|
|
// DOMContentLoaded 事件监听器
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// 初始化代理域名
|
|
initProxyDomain();
|
|
|
|
// 确保元素存在再添加事件监听器
|
|
const searchInput = document.getElementById('searchInput');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('keypress', function(event) {
|
|
if (event.key === 'Enter') {
|
|
searchDockerHub(1);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 加载菜单
|
|
loadMenu();
|
|
|
|
// 统一调用文档加载函数
|
|
loadAndDisplayDocumentation();
|
|
});
|
|
</script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/2.0.3/marked.min.js"></script>
|
|
</body>
|
|
</html> |