Files
Docker-Proxy/hubcmdui/web/index.html
T
2025-04-02 06:51:11 +08:00

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>