Files
Docker-Proxy/hubcmdui/web/index.html

1950 lines
91 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-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 id="mainLogo" src="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" alt="Logo" class="logo" style="opacity: 0; transition: opacity 0.3s ease;">
</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>`;
}
}
window.protectCopyright = protectCopyright;
// ========================================
// === 文档加载相关函数 (移到此处) ===
// ========================================
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>
`;
}
}
}
//
function useImage(imageName) {
// 切换到镜像加速标签页
switchTab('accelerate');
// 填充镜像名称到输入框
const imageInput = document.getElementById('imageInput');
if (imageInput) {
imageInput.value = imageName;
// 自动生成加速命令
generateCommands(imageName);
// 滚动到结果区域
const resultDiv = document.getElementById('result');
if (resultDiv) {
resultDiv.scrollIntoView({ behavior: 'smooth' });
}
}
// 显示用户友好的提示
showToastNotification(`已选择镜像: ${imageName}`, 'success');
}
window.useImage = useImage;
// ========================================
// === 全局变量和状态 ===
// ========================================
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') {
// 重置显示状态
const quickGuideEl = document.querySelector('.quick-guide');
if (quickGuideEl) quickGuideEl.style.display = 'block';
const popularImagesEl = document.querySelector('.popular-images');
if (popularImagesEl) popularImagesEl.style.display = 'block';
const accelerateFeaturesEl = document.querySelector('#accelerateContent .features');
if (accelerateFeaturesEl) accelerateFeaturesEl.style.display = 'grid';
const resultEl = document.getElementById('result');
if (resultEl) resultEl.style.display = 'none';
// 清空搜索相关的输入和结果,因为我们切换到了加速标签
const searchInputEl = document.getElementById('searchInput');
if(searchInputEl) searchInputEl.value = '';
const searchResultsEl = document.getElementById('searchResults');
if(searchResultsEl) searchResultsEl.innerHTML = '';
}
}
window.switchTab = switchTab;
// 新增:返回搜索结果视图
function showSearchResults() {
const searchResultsList = document.getElementById('searchResultsList');
const imageTagsView = document.getElementById('imageTagsView');
const searchResults = document.getElementById('searchResults');
const paginationContainer = document.getElementById('paginationContainer');
const features = document.querySelector('#searchContent .features'); // 获取特性区域
if (searchResultsList) searchResultsList.style.display = 'block';
if (imageTagsView) imageTagsView.style.display = 'none';
// 检查 searchResults 是否有内容并且不是 "未找到" 消息
if (searchResults && searchResults.innerHTML.trim() !== '' && !searchResults.querySelector('.empty-result')) {
searchResults.style.display = 'block';
if (paginationContainer) paginationContainer.style.display = 'flex';
if (features) features.style.display = 'none'; // 隐藏特性区
} else {
// 如果 searchResults 为空, 或者包含 "未找到" 消息
if (searchResults) searchResults.style.display = 'block'; // 保持 searchResults 区域可见以显示 "未找到"
if (paginationContainer) paginationContainer.style.display = 'none';
if (features) features.style.display = 'grid'; // 显示特性区
}
}
window.showSearchResults = showSearchResults;
// 添加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(imageNameInput) {
let currentImageName = imageNameInput;
if (!currentImageName) {
const imageInputEl = document.getElementById('imageInput');
if (imageInputEl) currentImageName = imageInputEl.value.trim();
}
if (!currentImageName) {
alert('请输入 Docker 镜像名称');
return;
}
let [imageName, tag] = currentImageName.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);
});
// 显示结果并隐藏其他内容
if (resultDiv) {
resultDiv.style.display = 'flex';
resultDiv.style.flexDirection = 'column';
}
const quickGuideEl = document.querySelector('.quick-guide');
if (quickGuideEl) quickGuideEl.style.display = 'none';
const accelerateFeaturesEl = document.querySelector('#accelerateContent .features');
if (accelerateFeaturesEl) accelerateFeaturesEl.style.display = 'none';
}
window.generateCommands = generateCommands;
// 复制命令到剪贴板
function copyToClipboard(text, element) {
// console.log('[copyToClipboard] Received text to copy:', text); // Debug log
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
showToastNotification('已复制到剪贴板', 'success');
}, (err) => {
console.error('无法复制文本: ', err);
showToastNotification('复制失败: ' + err.message, 'error');
});
} else {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showToastNotification('已复制到剪贴板', 'success');
} catch (err) {
console.error('无法使用 execCommand 复制文本: ', err);
showToastNotification('复制失败: ' + err.message, 'error');
} finally {
document.body.removeChild(textarea);
}
}
}
window.copyToClipboard = copyToClipboard;
// 改进的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';
}
}
window.searchDockerHub = searchDockerHub;
// 更新分页控件
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).replace(/'/g, "\\'")}')">
<i class="fas fa-rocket"></i> 使用此镜像
</button>
<button class="action-btn secondary" onclick="viewImageDetails('${(result.name || result.repo_name).replace(/'/g, "\\'")}', ${isOfficial}, '${encodeURIComponent(description).replace(/'/g, "%27")}', ${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.replace(/'/g, "\\'")}', ${currentImageData.isOfficial}, '${encodeURIComponent(currentImageData.description).replace(/'/g, "%27")}', ${currentImageData.stars}, ${currentImageData.pulls})" class="retry-btn">
<button class="retry-btn">
<i class="fas fa-redo"></i> 重试
</button>
</div>
</div>
`;
showToastNotification(`加载镜像详情失败: ${error.message}`, 'error');
}
}
window.viewImageDetails = viewImageDetails;
// 新增: 加载所有标签 - 改进错误处理
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';
}
}
window.loadAllTags = loadAllTags;
// 添加 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');
}
}
window.loadImageTags = loadImageTags;
// 新增: 显示客户端分页控制器
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 + '" 的标签</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();
}
}
window.filterTags = filterTags;
// 添加重置搜索功能
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);
}
}
window.resetTagSearch = resetTagSearch;
// 修改标签搜索容器,添加重置按钮
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) {
// 清理之前的返回顶部按钮
const existingBackToTopBtn = document.querySelector('.back-to-top-btn');
if (existingBackToTopBtn) {
existingBackToTopBtn.remove();
}
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);
}
}
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;
if (doc.content) {
// 使用marked渲染Markdown内容
if (window.marked) {
try {
// 配置marked选项以获得更好的渲染效果
marked.setOptions({
highlight: function(code, lang) {
// 如果有语法高亮库,可以在这里使用
return code;
},
langPrefix: 'language-',
breaks: true,
gfm: true
});
const rawHtml = marked.parse(doc.content);
// 创建一个临时的根元素来容纳和处理已解析的Markdown内容
const docFragmentRoot = document.createElement('div');
docFragmentRoot.innerHTML = rawHtml;
// 为代码块添加语言标识和复制按钮
const preElements = docFragmentRoot.querySelectorAll('pre');
preElements.forEach((preElement, index) => {
const codeElement = preElement.querySelector('code');
let codeToCopy = '';
let language = 'Code';
if (codeElement) {
codeToCopy = codeElement.textContent;
// 尝试从className获取语言信息
const className = codeElement.className;
const langMatch = className.match(/language-(\w+)/);
if (langMatch) {
language = langMatch[1].toUpperCase();
}
} else {
codeToCopy = preElement.textContent;
}
// 设置语言属性用于CSS显示
preElement.setAttribute('data-language', language);
if (codeToCopy.trim() !== '') {
const copyButton = document.createElement('button');
copyButton.className = 'copy-btn';
copyButton.innerHTML = '<i class="fas fa-copy"></i> 复制';
copyButton.onclick = function() {
copyToClipboard(codeToCopy, this);
};
preElement.appendChild(copyButton);
}
});
// 为链接添加外部链接图标
const links = docFragmentRoot.querySelectorAll('a');
links.forEach(link => {
const href = link.getAttribute('href');
if (href && (href.startsWith('http') || href.startsWith('https'))) {
link.innerHTML += ' <i class="fas fa-external-link-alt" style="font-size: 0.8em; margin-left: 0.25rem;"></i>';
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
}
});
// 为表格添加响应式包装
const tables = docFragmentRoot.querySelectorAll('table');
tables.forEach(table => {
const wrapper = document.createElement('div');
wrapper.className = 'table-wrapper';
wrapper.style.overflowX = 'auto';
wrapper.style.marginBottom = '1.5rem';
table.parentNode.insertBefore(wrapper, table);
wrapper.appendChild(table);
});
// 清空页面上的主容器
container.innerHTML = '';
// 创建文档头部
const docHeader = document.createElement('div');
docHeader.className = 'doc-header';
docHeader.innerHTML = `
<h1>${doc.title || '文档标题'}</h1>
${doc.description ? `<p class="doc-description">${doc.description}</p>` : ''}
`;
container.appendChild(docHeader);
// 创建 .doc-content div 并将处理过的文档片段追加进去
const docContentDiv = document.createElement('div');
docContentDiv.className = 'doc-content';
// 将 docFragmentRoot 的所有子节点移动到 docContentDiv
while (docFragmentRoot.firstChild) {
docContentDiv.appendChild(docFragmentRoot.firstChild);
}
container.appendChild(docContentDiv);
// 创建并追加 .doc-meta div
const docMetaDiv = document.createElement('div');
docMetaDiv.className = 'doc-meta';
const updateTime = doc.lastUpdated || doc.updatedAt || doc.updated_at;
if (updateTime) {
const formattedDate = new Date(updateTime).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
docMetaDiv.innerHTML = `
<i class="fas fa-clock"></i>
<span>最后更新: ${formattedDate}</span>
`;
}
container.appendChild(docMetaDiv);
// 添加返回顶部按钮(如果内容很长)
if (docContentDiv.scrollHeight > 1000) {
const backToTopBtn = document.createElement('button');
backToTopBtn.className = 'back-to-top-btn';
backToTopBtn.innerHTML = '<i class="fas fa-arrow-up"></i>';
backToTopBtn.style.cssText = `
position: fixed;
bottom: 2rem;
right: 2rem;
width: 3rem;
height: 3rem;
border-radius: 50%;
background: var(--primary-color);
color: white;
border: none;
cursor: pointer;
box-shadow: 0 4px 12px rgba(61, 124, 244, 0.3);
z-index: 1000;
opacity: 0.8;
transition: all 0.3s ease;
`;
backToTopBtn.onclick = () => {
container.scrollIntoView({ behavior: 'smooth' });
};
backToTopBtn.onmouseenter = () => {
backToTopBtn.style.opacity = '1';
backToTopBtn.style.transform = 'scale(1.1)';
};
backToTopBtn.onmouseleave = () => {
backToTopBtn.style.opacity = '0.8';
backToTopBtn.style.transform = 'scale(1)';
};
document.body.appendChild(backToTopBtn);
// 当切换文档时清理按钮
container.setAttribute('data-back-to-top', 'true');
}
} catch (error) {
console.error('Markdown解析失败:', error);
// 发生错误时的降级处理
container.innerHTML = `
<div class="doc-header">
<h1>${doc.title || '文档标题'}</h1>
</div>
<div class="doc-content">
<div class="error-container">
<i class="fas fa-exclamation-triangle"></i>
<h3>内容解析失败</h3>
<p>无法正确解析文档内容,显示原始内容:</p>
<pre><code>${doc.content}</code></pre>
</div>
</div>
<div class="doc-meta">
${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
</div>
`;
}
} else {
// marked 不可用时的降级处理
container.innerHTML = `
<div class="doc-header">
<h1>${doc.title || '文档标题'}</h1>
</div>
<div class="doc-content">
<div class="markdown-fallback">
<p><em>Markdown 解析器未加载,显示原始内容:</em></p>
<pre><code>${doc.content}</code></pre>
</div>
</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() {
// 加载系统配置(包括 logo
loadSystemConfig();
// 初始化代理域名
initProxyDomain();
// 确保元素存在再添加事件监听器
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
searchDockerHub(1);
}
});
}
// 加载菜单
loadMenu();
// 统一调用文档加载函数
loadAndDisplayDocumentation();
});
// 加载系统配置
function loadSystemConfig() {
fetch('/api/config')
.then(response => {
if (response.ok) {
return response.json();
}
// 如果配置加载失败,使用默认配置
return {};
})
.then(config => {
const logoElement = document.getElementById('mainLogo');
if (logoElement) {
// 如果有自定义logo配置且不为空则使用自定义logo
if (config.logo && config.logo.trim() !== '') {
logoElement.src = config.logo;
}
// 如果没有配置或为空保持默认logo不变
// 显示logo无论是默认还是自定义
logoElement.style.opacity = '1';
}
})
.catch(error => {
// 如果出错也要显示默认logo
console.warn('加载配置失败:', error);
const logoElement = document.getElementById('mainLogo');
if (logoElement) {
logoElement.style.opacity = '1';
}
});
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/2.0.3/marked.min.js"></script>
</body>
</html>