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