mirror of
https://github.com/dqzboy/Docker-Proxy.git
synced 2026-01-12 16:25:42 +08:00
1793 lines
83 KiB
HTML
1793 lines
83 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>`;
|
||
}
|
||
}
|
||
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) {
|
||
// 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);
|
||
}
|
||
}
|
||
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 rawHtml = marked.parse(doc.content);
|
||
|
||
// 创建一个临时的根元素来容纳和处理已解析的Markdown内容
|
||
const docFragmentRoot = document.createElement('div');
|
||
docFragmentRoot.innerHTML = rawHtml;
|
||
|
||
// 在这个临时根元素中查找所有的 <pre> 元素
|
||
const preElements = docFragmentRoot.querySelectorAll('pre');
|
||
preElements.forEach(preElement => {
|
||
const codeElement = preElement.querySelector('code');
|
||
let codeToCopy = '';
|
||
if (codeElement) {
|
||
codeToCopy = codeElement.textContent;
|
||
} else {
|
||
codeToCopy = preElement.textContent;
|
||
}
|
||
|
||
if (codeToCopy.trim() !== '') {
|
||
const copyButton = document.createElement('button');
|
||
copyButton.className = 'copy-btn'; // 应用现有样式
|
||
copyButton.textContent = '复制';
|
||
copyButton.onclick = function() { // 事件监听器在此处附加到按钮对象
|
||
// console.log('[Tutorial Copy Button] Attempting to copy:', codeToCopy); // 保留此调试日志
|
||
copyToClipboard(codeToCopy, this);
|
||
};
|
||
preElement.style.position = 'relative';
|
||
preElement.appendChild(copyButton); // 按钮被追加到 docFragmentRoot 内的 preElement
|
||
}
|
||
});
|
||
|
||
// 清空页面上的主容器
|
||
container.innerHTML = '';
|
||
|
||
// 创建 .doc-content div 并将处理过的文档片段追加进去
|
||
const docContentDiv = document.createElement('div');
|
||
docContentDiv.className = 'doc-content';
|
||
// 将 docFragmentRoot 的所有子节点移动到 docContentDiv,以避免多余的包裹 div
|
||
while (docFragmentRoot.firstChild) {
|
||
docContentDiv.appendChild(docFragmentRoot.firstChild);
|
||
}
|
||
container.appendChild(docContentDiv); // docContentDiv 现在包含带有活动按钮的 PRE 元素
|
||
|
||
// 创建并追加 .doc-meta div
|
||
const docMetaDiv = document.createElement('div');
|
||
docMetaDiv.className = 'doc-meta';
|
||
docMetaDiv.innerHTML = `${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}`;
|
||
container.appendChild(docMetaDiv);
|
||
|
||
} 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>
|