index.html 91 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Docker镜像加速服务</title>
  7. <link rel="icon" href="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" type="image/png">
  8. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  9. <link rel="stylesheet" href="style.css">
  10. <script src="js/nav-menu.js"></script>
  11. </head>
  12. <body>
  13. <header class="header">
  14. <div class="header-content">
  15. <a href="/" class="logo-link">
  16. <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;">
  17. </a>
  18. <nav class="nav-menu" id="navMenu">
  19. <!-- 菜单项通过 JavaScript 动态加载 -->
  20. </nav>
  21. </div>
  22. </header>
  23. <div class="container">
  24. <h1 class="page-title">Docker镜像加速服务</h1>
  25. <p class="page-subtitle">快速拉取 Docker 镜像,无需担心网络问题,轻松部署你的容器应用</p>
  26. <div class="tab-container">
  27. <div class="tab active" onclick="switchTab('accelerate')">
  28. <i class="fas fa-rocket"></i> 镜像加速
  29. </div>
  30. <div class="tab" onclick="switchTab('search')">
  31. <i class="fas fa-search"></i> 镜像搜索
  32. </div>
  33. <div class="tab" onclick="switchTab('documentation')">
  34. <i class="fas fa-book"></i> 使用教程
  35. </div>
  36. </div>
  37. <!-- 镜像加速内容 -->
  38. <div id="accelerateContent" class="content active">
  39. <div class="input-group">
  40. <input type="text" id="imageInput"
  41. placeholder="输入镜像名称,例如:nginx 或 mysql:5.7"
  42. onkeypress="if(event.key === 'Enter') generateCommands()"
  43. autofocus>
  44. <button onclick="generateCommands()">
  45. <i class="fas fa-bolt"></i> 获取加速命令
  46. </button>
  47. </div>
  48. <div id="result" style="display:none;">
  49. <h2><i class="fas fa-terminal"></i> 加速命令</h2>
  50. <div id="commandsContainer"></div>
  51. </div>
  52. <div class="features">
  53. <div class="feature-card">
  54. <i class="fas fa-tachometer-alt"></i>
  55. <h3>高速拉取</h3>
  56. <p>通过优化的代理网络,加速Docker镜像拉取</p>
  57. </div>
  58. <div class="feature-card">
  59. <i class="fas fa-shield-alt"></i>
  60. <h3>稳定可靠</h3>
  61. <p>解决网络问题导致的拉取失败,提高部署成功率</p>
  62. </div>
  63. <div class="feature-card">
  64. <i class="fas fa-magic"></i>
  65. <h3>简单易用</h3>
  66. <p>一键生成加速命令,无需复杂配置,立即开始使用</p>
  67. </div>
  68. </div>
  69. </div>
  70. <!-- 搜索内容 -->
  71. <div id="searchContent" class="content">
  72. <div class="search-container">
  73. <input type="text" id="searchInput"
  74. placeholder="输入关键词搜索Docker镜像,例如:nginx、mysql、redis..."
  75. onkeypress="if(event.key === 'Enter') searchDockerHub(1)">
  76. <button onclick="searchDockerHub(1)">
  77. <i class="fas fa-search"></i> 搜索镜像
  78. </button>
  79. </div>
  80. <!-- 搜索结果容器 -->
  81. <div id="searchResultsContainer">
  82. <!-- 搜索结果列表 -->
  83. <div id="searchResultsList">
  84. <div id="searchResults"></div>
  85. <!-- 分页控件 -->
  86. <div class="pagination-container" id="paginationContainer" style="display: none;">
  87. <button id="prevPageBtn" onclick="searchDockerHub(currentPage - 1)" disabled>
  88. <i class="fas fa-chevron-left"></i> 上一页
  89. </button>
  90. <span id="pageInfo">第 1 页</span>
  91. <button id="nextPageBtn" onclick="searchDockerHub(currentPage + 1)">
  92. 下一页 <i class="fas fa-chevron-right"></i>
  93. </button>
  94. </div>
  95. </div>
  96. <!-- 标签视图 -->
  97. <div id="imageTagsView" style="display: none;" class="image-tags-view">
  98. <div class="tag-header">
  99. <div class="tag-breadcrumb">
  100. <a href="javascript:void(0);" onclick="showSearchResults()">返回搜索结果</a>
  101. </div>
  102. <h2 id="currentImageTitle"></h2>
  103. <p id="imageDescription" class="image-description"></p>
  104. <div class="image-meta">
  105. <span id="imageStars"></span>
  106. <span id="imagePulls"></span>
  107. </div>
  108. </div>
  109. <div class="tag-search-container">
  110. <input type="text" id="tagSearchInput" placeholder="搜索TAG..." onkeyup="filterTags()">
  111. </div>
  112. <div id="tagsResults"></div>
  113. <div class="pagination-container" id="tagPaginationContainer" style="display: none;">
  114. <button id="tagPrevPageBtn" onclick="loadImageTags(currentTagPage - 1)" disabled>
  115. <i class="fas fa-chevron-left"></i> 上一页
  116. </button>
  117. <span id="tagPageInfo">第 1 页</span>
  118. <button id="tagNextPageBtn" onclick="loadImageTags(currentTagPage + 1)">
  119. 下一页 <i class="fas fa-chevron-right"></i>
  120. </button>
  121. </div>
  122. </div>
  123. </div>
  124. <!-- 底部特性说明 -->
  125. <div class="features">
  126. <div class="feature-card">
  127. <i class="fas fa-search"></i>
  128. <h3>快速搜索</h3>
  129. <p>便捷地搜索Docker Hub上的所有可用镜像</p>
  130. </div>
  131. <div class="feature-card">
  132. <i class="fas fa-tag"></i>
  133. <h3>版本管理</h3>
  134. <p>查看所有可用的镜像标签和版本信息</p>
  135. </div>
  136. <div class="feature-card">
  137. <i class="fas fa-rocket"></i>
  138. <h3>一键部署</h3>
  139. <p>快速获取并使用所需的Docker镜像</p>
  140. </div>
  141. </div>
  142. </div>
  143. <!-- 文档内容 -->
  144. <div id="documentationContent" class="content">
  145. <div id="documentList"></div>
  146. <div id="documentationText"></div>
  147. </div>
  148. </div>
  149. <footer class="footer">
  150. <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>
  151. </footer>
  152. <script>
  153. // 设置当前年份
  154. document.getElementById('currentYear').textContent = new Date().getFullYear();
  155. document.addEventListener('DOMContentLoaded', (event) => {
  156. // 版权保护
  157. protectCopyright();
  158. });
  159. // 版权保护函数
  160. function protectCopyright() {
  161. const footer = document.querySelector('.footer');
  162. const expectedText = 'Docker-Proxy';
  163. const expectedLink = 'https://github.com/dqzboy/Docker-Proxy';
  164. // 初始检查
  165. validateCopyright();
  166. // 定期检查版权信息
  167. setInterval(validateCopyright, 2000);
  168. function validateCopyright() {
  169. const copyrightText = document.querySelector('.copyright-text');
  170. const githubLink = document.querySelector('.footer a');
  171. if (!copyrightText || copyrightText.textContent !== expectedText ||
  172. !githubLink || githubLink.href !== expectedLink) {
  173. // 版权信息被篡改,恢复
  174. restoreCopyright();
  175. }
  176. }
  177. function restoreCopyright() {
  178. 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>`;
  179. }
  180. }
  181. window.protectCopyright = protectCopyright;
  182. // ========================================
  183. // === 文档加载相关函数 (移到此处) ===
  184. // ========================================
  185. let documentationLoaded = false;
  186. async function loadAndDisplayDocumentation() {
  187. // 防止重复加载
  188. if (documentationLoaded) {
  189. // console.log('文档已加载,跳过重复加载');
  190. return;
  191. }
  192. const docListContainer = document.getElementById('documentList');
  193. const docContentContainer = document.getElementById('documentationText');
  194. if (!docListContainer || !docContentContainer) {
  195. // console.warn('找不到文档列表或内容容器,可能不是文档页面');
  196. return; // 如果容器不存在,则不执行加载
  197. }
  198. try {
  199. // console.log('开始加载文档列表和内容...');
  200. // 显示加载状态
  201. docListContainer.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 正在加载文档列表...</div>';
  202. docContentContainer.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 请从左侧选择文档...</div>';
  203. // 获取文档列表
  204. const response = await fetch('/api/documentation');
  205. if (!response.ok) {
  206. throw new Error(`获取文档列表失败: ${response.status}`);
  207. }
  208. const data = await response.json();
  209. // console.log('获取到文档列表:', data);
  210. // 保存到全局变量
  211. window.documentationData = data;
  212. documentationLoaded = true; // 标记为已加载
  213. if (!Array.isArray(data) || data.length === 0) {
  214. docListContainer.innerHTML = `
  215. <h2>文档目录</h2>
  216. <div class="empty-list">
  217. <i class="fas fa-file-alt fa-3x"></i>
  218. <p>暂无文档</p>
  219. </div>
  220. `;
  221. docContentContainer.innerHTML = `
  222. <div class="empty-content">
  223. <i class="fas fa-file-alt fa-3x"></i>
  224. <h2>暂无文档</h2>
  225. <p>系统中还没有添加任何使用教程文档。</p>
  226. </div>
  227. `;
  228. return;
  229. }
  230. // 创建文档列表
  231. let html = '<h2>文档目录</h2><ul class="doc-list">';
  232. data.forEach((doc, index) => {
  233. // 确保doc有效
  234. if (doc && doc.id && doc.title) {
  235. html += `
  236. <li class="doc-item" data-id="${doc.id}">
  237. <a href="javascript:void(0)" onclick="showDocument(${index})">
  238. <i class="fas fa-file-alt"></i>
  239. <span>${doc.title}</span>
  240. </a>
  241. </li>
  242. `;
  243. } else {
  244. console.warn('发现无效的文档数据:', doc);
  245. }
  246. });
  247. html += '</ul>';
  248. docListContainer.innerHTML = html;
  249. // 默认加载第一篇文档
  250. if (data.length > 0 && data[0]) {
  251. showDocument(0);
  252. // 激活第一个列表项
  253. const firstLink = docListContainer.querySelector('.doc-item a');
  254. if (firstLink) {
  255. firstLink.classList.add('active');
  256. }
  257. } else {
  258. // 如果第一个文档无效,显示空状态
  259. docContentContainer.innerHTML = `
  260. <div class="empty-content">
  261. <i class="fas fa-file-alt fa-3x"></i>
  262. <p>请从左侧选择一篇文档查看</p>
  263. </div>
  264. `;
  265. }
  266. } catch (error) {
  267. console.error('加载文档列表失败:', error);
  268. documentationLoaded = false; // 加载失败,允许重试
  269. if (docListContainer) {
  270. docListContainer.innerHTML = `
  271. <h2>文档目录</h2>
  272. <div class="error-item">
  273. <i class="fas fa-exclamation-triangle"></i>
  274. <p>${error.message}</p>
  275. <button class="btn btn-sm btn-primary mt-2" onclick="loadAndDisplayDocumentation()">重试</button>
  276. </div>
  277. `;
  278. }
  279. if (docContentContainer) {
  280. docContentContainer.innerHTML = `
  281. <div class="error-container">
  282. <i class="fas fa-exclamation-triangle fa-3x"></i>
  283. <h2>加载失败</h2>
  284. <p>无法获取文档列表: ${error.message}</p>
  285. </div>
  286. `;
  287. }
  288. }
  289. }
  290. //
  291. function useImage(imageName) {
  292. // 切换到镜像加速标签页
  293. switchTab('accelerate');
  294. // 填充镜像名称到输入框
  295. const imageInput = document.getElementById('imageInput');
  296. if (imageInput) {
  297. imageInput.value = imageName;
  298. // 自动生成加速命令
  299. generateCommands(imageName);
  300. // 滚动到结果区域
  301. const resultDiv = document.getElementById('result');
  302. if (resultDiv) {
  303. resultDiv.scrollIntoView({ behavior: 'smooth' });
  304. }
  305. }
  306. // 显示用户友好的提示
  307. showToastNotification(`已选择镜像: ${imageName}`, 'success');
  308. }
  309. window.useImage = useImage;
  310. // ========================================
  311. // === 全局变量和状态 ===
  312. // ========================================
  313. let proxyDomain = '';
  314. let currentIndex = 0;
  315. let items = [];
  316. let currentPage = 1;
  317. let currentSearchTerm = '';
  318. let totalPages = 1;
  319. let currentTagPage = 1;
  320. let currentImageData = null;
  321. // 初始化时加载代理域名配置
  322. async function initProxyDomain() {
  323. try {
  324. const response = await fetch('/api/config');
  325. if (response.ok) {
  326. const config = await response.json();
  327. if (config.proxyDomain) {
  328. proxyDomain = config.proxyDomain;
  329. // console.log('成功加载代理域名:', proxyDomain);
  330. } else {
  331. console.warn('配置中没有proxyDomain字段');
  332. proxyDomain = 'registry-1.docker.io'; // 使用默认值
  333. }
  334. } else {
  335. console.error('加载配置失败:', response.status, response.statusText);
  336. proxyDomain = 'registry-1.docker.io'; // 使用默认值
  337. }
  338. } catch (error) {
  339. console.error('初始化代理域名失败:', error);
  340. proxyDomain = 'registry-1.docker.io'; // 使用默认值
  341. }
  342. }
  343. // ========================================
  344. // === 全局提示函数 ===
  345. // ========================================
  346. function showToastNotification(message, type = 'info') { // types: info, success, error
  347. // 移除任何现有的通知
  348. const existingNotification = document.querySelector('.toast-notification');
  349. if (existingNotification) {
  350. existingNotification.remove();
  351. }
  352. // 创建新的通知元素
  353. const toast = document.createElement('div');
  354. toast.className = `toast-notification ${type}`;
  355. // 设置图标和内容
  356. let iconClass = 'fas fa-info-circle';
  357. if (type === 'success') iconClass = 'fas fa-check-circle';
  358. if (type === 'error') iconClass = 'fas fa-exclamation-circle';
  359. toast.innerHTML = `<i class="${iconClass}"></i> ${message}`;
  360. document.body.appendChild(toast);
  361. // 动画效果 (如果需要的话,可以在CSS中定义 @keyframes fadeIn)
  362. // toast.style.animation = 'fadeIn 0.3s ease-in';
  363. // 设定时间后自动移除
  364. setTimeout(() => {
  365. toast.style.opacity = '0'; // 开始淡出
  366. toast.style.transition = 'opacity 0.3s ease-out';
  367. setTimeout(() => toast.remove(), 300); // 淡出后移除DOM
  368. }, 3500); // 显示 3.5 秒
  369. }
  370. // ========================================
  371. // === 其他函数定义 ===
  372. // ========================================
  373. // 标签切换功能
  374. function switchTab(tabName) {
  375. const tabs = document.querySelectorAll('.tab');
  376. const contents = document.querySelectorAll('.content');
  377. const features = document.querySelector('#searchContent .features');
  378. tabs.forEach(tab => tab.classList.remove('active'));
  379. contents.forEach(content => content.classList.remove('active'));
  380. // 更新为支持3个选项卡
  381. let tabIndex = 1;
  382. if (tabName === 'search') {
  383. tabIndex = 2;
  384. // 只有在没有搜索结果时显示底部特性说明
  385. const searchResults = document.getElementById('searchResults');
  386. if (!searchResults.innerHTML.trim()) {
  387. features.style.display = 'grid';
  388. }
  389. } else if (tabName === 'documentation') {
  390. tabIndex = 3;
  391. }
  392. document.querySelector(`.tab:nth-child(${tabIndex})`).classList.add('active');
  393. document.getElementById(`${tabName}Content`).classList.add('active');
  394. // 重置显示
  395. if (document.getElementById('searchResultsContainer')) {
  396. document.getElementById('searchResultsContainer').style.display = 'block';
  397. }
  398. if (document.getElementById('searchResultsList')) {
  399. document.getElementById('searchResultsList').style.display = 'block';
  400. }
  401. if (document.getElementById('imageTagsView')) {
  402. document.getElementById('imageTagsView').style.display = 'none';
  403. }
  404. document.getElementById('result').style.display = 'none';
  405. document.getElementById('searchResults').style.display = 'none';
  406. document.getElementById('paginationContainer').style.display = 'none';
  407. if (tabName === 'documentation') {
  408. loadAndDisplayDocumentation();
  409. } else if (tabName === 'accelerate') {
  410. // 重置显示状态
  411. const quickGuideEl = document.querySelector('.quick-guide');
  412. if (quickGuideEl) quickGuideEl.style.display = 'block';
  413. const popularImagesEl = document.querySelector('.popular-images');
  414. if (popularImagesEl) popularImagesEl.style.display = 'block';
  415. const accelerateFeaturesEl = document.querySelector('#accelerateContent .features');
  416. if (accelerateFeaturesEl) accelerateFeaturesEl.style.display = 'grid';
  417. const resultEl = document.getElementById('result');
  418. if (resultEl) resultEl.style.display = 'none';
  419. // 清空搜索相关的输入和结果,因为我们切换到了加速标签
  420. const searchInputEl = document.getElementById('searchInput');
  421. if(searchInputEl) searchInputEl.value = '';
  422. const searchResultsEl = document.getElementById('searchResults');
  423. if(searchResultsEl) searchResultsEl.innerHTML = '';
  424. }
  425. }
  426. window.switchTab = switchTab;
  427. // 新增:返回搜索结果视图
  428. function showSearchResults() {
  429. const searchResultsList = document.getElementById('searchResultsList');
  430. const imageTagsView = document.getElementById('imageTagsView');
  431. const searchResults = document.getElementById('searchResults');
  432. const paginationContainer = document.getElementById('paginationContainer');
  433. const features = document.querySelector('#searchContent .features'); // 获取特性区域
  434. if (searchResultsList) searchResultsList.style.display = 'block';
  435. if (imageTagsView) imageTagsView.style.display = 'none';
  436. // 检查 searchResults 是否有内容并且不是 "未找到" 消息
  437. if (searchResults && searchResults.innerHTML.trim() !== '' && !searchResults.querySelector('.empty-result')) {
  438. searchResults.style.display = 'block';
  439. if (paginationContainer) paginationContainer.style.display = 'flex';
  440. if (features) features.style.display = 'none'; // 隐藏特性区
  441. } else {
  442. // 如果 searchResults 为空, 或者包含 "未找到" 消息
  443. if (searchResults) searchResults.style.display = 'block'; // 保持 searchResults 区域可见以显示 "未找到"
  444. if (paginationContainer) paginationContainer.style.display = 'none';
  445. if (features) features.style.display = 'grid'; // 显示特性区
  446. }
  447. }
  448. window.showSearchResults = showSearchResults;
  449. // 添加formatNumber函数定义
  450. function formatNumber(num) {
  451. if (num >= 1000000000) {
  452. return (num >= 1500000000 ? '1B+' : '1B');
  453. } else if (num >= 1000000) {
  454. const m = Math.floor(num / 1000000);
  455. return (m >= 100 ? '100M+' : m + 'M');
  456. } else if (num >= 1000) {
  457. const k = Math.floor(num / 1000);
  458. return (k >= 100 ? '100K+' : k + 'K');
  459. }
  460. return num.toString();
  461. }
  462. // 生成加速命令
  463. function generateCommands(imageNameInput) {
  464. let currentImageName = imageNameInput;
  465. if (!currentImageName) {
  466. const imageInputEl = document.getElementById('imageInput');
  467. if (imageInputEl) currentImageName = imageInputEl.value.trim();
  468. }
  469. if (!currentImageName) {
  470. alert('请输入 Docker 镜像名称');
  471. return;
  472. }
  473. let [imageName, tag] = currentImageName.split(':');
  474. tag = tag || 'latest';
  475. let originalImage = `${imageName}:${tag}`;
  476. let proxyImage = '';
  477. if (!imageName.includes('/')) {
  478. proxyImage = `${proxyDomain}/library/${imageName}:${tag}`;
  479. } else {
  480. proxyImage = `${proxyDomain}/${imageName}:${tag}`;
  481. }
  482. const commands = [
  483. { title: "代理拉取镜像", cmd: `docker pull ${proxyImage}` },
  484. { title: "原始拉取命令", cmd: `docker pull ${originalImage}` },
  485. { title: "重命名镜像", cmd: `docker tag ${proxyImage} ${originalImage}` },
  486. { title: "删除代理镜像", cmd: `docker rmi ${proxyImage}` }
  487. ];
  488. const resultDiv = document.getElementById('result');
  489. const container = document.getElementById('commandsContainer');
  490. container.innerHTML = '';
  491. // 将生成的命令添加到结果容器中
  492. commands.forEach((command, index) => {
  493. const cmdDiv = document.createElement('div');
  494. cmdDiv.className = 'step';
  495. cmdDiv.innerHTML = `
  496. <h3>${index + 1}. ${command.title}</h3>
  497. <div class="command-terminal">
  498. <div class="terminal-header">
  499. <div class="terminal-button button-red"></div>
  500. <div class="terminal-button button-yellow"></div>
  501. <div class="terminal-button button-green"></div>
  502. </div>
  503. <pre><code>${command.cmd}</code>
  504. <button class="copy-btn" onclick="copyToClipboard('${command.cmd}', this)">复制</button>
  505. </pre>
  506. </div>
  507. `;
  508. container.appendChild(cmdDiv);
  509. });
  510. // 显示结果并隐藏其他内容
  511. if (resultDiv) {
  512. resultDiv.style.display = 'flex';
  513. resultDiv.style.flexDirection = 'column';
  514. }
  515. const quickGuideEl = document.querySelector('.quick-guide');
  516. if (quickGuideEl) quickGuideEl.style.display = 'none';
  517. const accelerateFeaturesEl = document.querySelector('#accelerateContent .features');
  518. if (accelerateFeaturesEl) accelerateFeaturesEl.style.display = 'none';
  519. }
  520. window.generateCommands = generateCommands;
  521. // 复制命令到剪贴板
  522. function copyToClipboard(text, element) {
  523. // console.log('[copyToClipboard] Received text to copy:', text); // Debug log
  524. if (navigator.clipboard && navigator.clipboard.writeText) {
  525. navigator.clipboard.writeText(text).then(() => {
  526. showToastNotification('已复制到剪贴板', 'success');
  527. }, (err) => {
  528. console.error('无法复制文本: ', err);
  529. showToastNotification('复制失败: ' + err.message, 'error');
  530. });
  531. } else {
  532. const textarea = document.createElement('textarea');
  533. textarea.value = text;
  534. document.body.appendChild(textarea);
  535. textarea.select();
  536. try {
  537. document.execCommand('copy');
  538. showToastNotification('已复制到剪贴板', 'success');
  539. } catch (err) {
  540. console.error('无法使用 execCommand 复制文本: ', err);
  541. showToastNotification('复制失败: ' + err.message, 'error');
  542. } finally {
  543. document.body.removeChild(textarea);
  544. }
  545. }
  546. }
  547. window.copyToClipboard = copyToClipboard;
  548. // 改进的API请求函数,支持自动重试
  549. async function fetchWithRetry(url, options = {}, retries = 3, retryDelay = 1000) {
  550. try {
  551. const response = await fetch(url, options);
  552. // 检查响应状态
  553. if (!response.ok) {
  554. const errorData = await response.json();
  555. throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
  556. }
  557. // 检查内容类型
  558. const contentType = response.headers.get("content-type");
  559. if (!contentType || !contentType.includes("application/json")) {
  560. throw new Error('服务器返回了非JSON格式的数据,请联系管理员');
  561. }
  562. return await response.json();
  563. } catch (error) {
  564. // 如果没有剩余重试次数,抛出异常
  565. if (retries <= 0) throw error;
  566. console.warn(`请求失败,将在${retryDelay}ms后重试 (剩余${retries}次): ${error.message}`);
  567. // 等待重试延迟
  568. await new Promise(resolve => setTimeout(resolve, retryDelay));
  569. // 递归重试,增加延迟时间
  570. return fetchWithRetry(url, options, retries - 1, retryDelay * 1.5);
  571. }
  572. }
  573. // 搜索功能 - 支持分页
  574. async function searchDockerHub(page = 1) {
  575. const searchTerm = document.getElementById('searchInput').value.trim();
  576. if (!searchTerm) {
  577. showToastNotification('请输入搜索关键词', 'info');
  578. return;
  579. }
  580. // 如果搜索词改变,重置为第1页
  581. if (currentSearchTerm !== searchTerm) {
  582. page = 1;
  583. currentSearchTerm = searchTerm;
  584. }
  585. currentPage = page;
  586. const searchResults = document.getElementById('searchResults');
  587. searchResults.innerHTML = '<div class="loading-indicator">正在搜索...</div>';
  588. searchResults.style.display = 'block'; // 确保搜索结果可见
  589. // 隐藏底部特性说明
  590. const features = document.querySelector('#searchContent .features');
  591. features.style.display = 'none';
  592. // 当执行搜索时,确保返回到搜索结果列表视图
  593. document.getElementById('searchResultsList').style.display = 'block';
  594. document.getElementById('imageTagsView').style.display = 'none';
  595. try {
  596. // console.log(`搜索Docker Hub: 关键词=${searchTerm}, 页码=${page}`);
  597. // 使用新的fetchWithRetry函数
  598. const data = await fetchWithRetry(
  599. `/api/dockerhub/search?term=${encodeURIComponent(searchTerm)}&page=${page}`
  600. );
  601. const results = data.results;
  602. const officialImages = results.filter(result => result.is_official);
  603. const unofficialImages = results.filter(result => !result.is_official)
  604. .sort((a, b) => (b.star_count || 0) - (a.star_count || 0));
  605. const totalCount = data.count || 0;
  606. totalPages = Math.ceil(totalCount / 25);
  607. if (data.results && data.results.length > 0) {
  608. searchResults.innerHTML = '';
  609. officialImages.forEach(result => {
  610. searchResults.appendChild(createResultItem(result, true));
  611. });
  612. unofficialImages.forEach(result => {
  613. searchResults.appendChild(createResultItem(result, false));
  614. });
  615. updatePagination(page, totalPages);
  616. document.getElementById('paginationContainer').style.display = 'flex';
  617. } else {
  618. searchResults.innerHTML = '<div class="empty-result"><i class="fas fa-search"></i><p>未找到匹配的镜像</p></div>';
  619. document.getElementById('paginationContainer').style.display = 'none';
  620. }
  621. } catch (error) {
  622. console.error('搜索出错:', error);
  623. searchResults.innerHTML = `
  624. <div class="error-message">
  625. <i class="fas fa-exclamation-circle"></i>
  626. <p>搜索时发生错误: ${error.message}</p>
  627. <button onclick="searchDockerHub(${page})" class="retry-btn">
  628. <i class="fas fa-redo"></i> 重试
  629. </button>
  630. </div>`;
  631. document.getElementById('paginationContainer').style.display = 'none';
  632. }
  633. }
  634. window.searchDockerHub = searchDockerHub;
  635. // 更新分页控件
  636. function updatePagination(currentPage, totalPages) {
  637. const paginationContainer = document.getElementById('paginationContainer');
  638. const prevBtn = document.getElementById('prevPageBtn');
  639. const nextBtn = document.getElementById('nextPageBtn');
  640. const pageInfo = document.getElementById('pageInfo');
  641. // 显示分页控件
  642. paginationContainer.style.display = 'flex';
  643. // 更新页码信息
  644. pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
  645. // 根据当前页码禁用或启用上一页/下一页按钮
  646. prevBtn.disabled = currentPage <= 1;
  647. nextBtn.disabled = currentPage >= totalPages;
  648. }
  649. // 更新TAG分页控件
  650. function updateTagPagination(currentPage, totalPages) {
  651. const paginationContainer = document.getElementById('tagPaginationContainer');
  652. const prevBtn = document.getElementById('tagPrevPageBtn');
  653. const nextBtn = document.getElementById('tagNextPageBtn');
  654. const pageInfo = document.getElementById('tagPageInfo');
  655. // 显示分页控件
  656. paginationContainer.style.display = 'flex';
  657. // 更新页码信息
  658. pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
  659. // 根据当前页码禁用或启用上一页/下一页按钮
  660. prevBtn.disabled = currentPage <= 1;
  661. nextBtn.disabled = currentPage >= totalPages;
  662. }
  663. function createResultItem(result, isOfficial) {
  664. const resultItem = document.createElement('div');
  665. resultItem.className = `search-result-item ${isOfficial ? 'official-image' : ''}`;
  666. // 确保获取正确的描述字段 - 修复描述信息缺失问题
  667. const description = result.description || result.short_description || '暂无描述';
  668. resultItem.innerHTML = `
  669. <div class="result-header">
  670. <div class="title-badge">
  671. <h3>${result.name || result.repo_name || '未知名称'}</h3>
  672. ${isOfficial ? '<span class="official-badge"><i class="fas fa-check-circle"></i> 官方</span>' : ''}
  673. </div>
  674. <div class="result-stats">
  675. <span class="stats"><i class="fas fa-star"></i> ${formatNumber(result.star_count || 0)}</span>
  676. <span class="stats"><i class="fas fa-download"></i> ${formatNumber(result.pull_count || 0)}</span>
  677. </div>
  678. </div>
  679. <p class="result-description">${description}</p>
  680. <div class="result-actions">
  681. <button class="action-btn primary" onclick="useImage('${(result.name || result.repo_name).replace(/'/g, "\\'")}')">
  682. <i class="fas fa-rocket"></i> 使用此镜像
  683. </button>
  684. <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})">
  685. <i class="fas fa-tags"></i> 查看标签
  686. </button>
  687. </div>
  688. `;
  689. return resultItem;
  690. }
  691. // 修改查看标签详情函数 - 改进错误处理
  692. async function viewImageDetails(imageName, isOfficial, description, stars, pulls) {
  693. // 保存当前镜像信息
  694. currentImageData = {
  695. name: imageName,
  696. isOfficial: isOfficial,
  697. description: decodeURIComponent(description || ''),
  698. stars: stars,
  699. pulls: pulls
  700. };
  701. // 显示加载中状态
  702. const imageTagsView = document.getElementById('imageTagsView');
  703. imageTagsView.innerHTML = '<div class="loading-container"><div class="loading-indicator">正在加载镜像信息...</div></div>';
  704. document.getElementById('searchResultsList').style.display = 'none';
  705. imageTagsView.style.display = 'block';
  706. try {
  707. // 使用新的fetchWithRetry函数获取标签计数
  708. const countApiUrl = `/api/dockerhub/tag-count?name=${encodeURIComponent(currentImageData.name)}&official=${currentImageData.isOfficial}`;
  709. // console.log('Requesting tag count from:', countApiUrl);
  710. const countData = await fetchWithRetry(countApiUrl);
  711. // console.log('Received tag count data:', countData);
  712. const tagCount = countData.count || 0;
  713. const recommendedMode = countData.recommended_mode || 'paginated';
  714. // 根据标签数量判断是否显示警告
  715. let warningMessage = '';
  716. let loadAllBtnDisabled = false;
  717. if (tagCount > 1000) {
  718. warningMessage = `<div class="tag-count-warning">
  719. <i class="fas fa-exclamation-triangle"></i>
  720. <p>该镜像包含 <strong>${tagCount}</strong> 个标签,加载全部可能会很慢。建议使用分页浏览或利用搜索功能查找特定标签。</p>
  721. </div>`;
  722. loadAllBtnDisabled = true;
  723. } else if (tagCount > 500) {
  724. warningMessage = `<div class="tag-count-warning moderate">
  725. <i class="fas fa-info-circle"></i>
  726. <p>该镜像包含 <strong>${tagCount}</strong> 个标签,加载全部可能需要一些时间。</p>
  727. </div>`;
  728. }
  729. // 重新构建标签视图内容
  730. imageTagsView.innerHTML = `
  731. <div class="tag-header">
  732. <div class="tag-breadcrumb">
  733. <a href="javascript:void(0);" onclick="showSearchResults()">返回搜索结果</a>
  734. </div>
  735. <h2 id="currentImageTitle">${imageName}</h2>
  736. <p id="imageDescription" class="image-description">${currentImageData.description || '暂无描述'}</p>
  737. <div class="image-meta">
  738. <span id="imageStars"><i class="fas fa-star"></i> ${formatNumber(currentImageData.stars || 0)} 星标</span>
  739. <span id="imagePulls"><i class="fas fa-download"></i> ${formatNumber(currentImageData.pulls || 0)} 下载</span>
  740. <span id="imageTags"><i class="fas fa-tags"></i> ${formatNumber(tagCount)} 个标签</span>
  741. </div>
  742. </div>
  743. ${warningMessage}
  744. <div class="tag-actions">
  745. <div class="tag-search-container">
  746. <input type="text" id="tagSearchInput" placeholder="搜索TAG..." onkeyup="filterTags()">
  747. </div>
  748. <button id="loadAllTagsBtn" class="load-all-btn" onclick="loadAllTags()" ${loadAllBtnDisabled ? 'disabled' : ''}>
  749. <i class="fas fa-cloud-download-alt"></i> 加载全部TAG
  750. </button>
  751. </div>
  752. <div id="tagsResults"></div>
  753. <div class="pagination-container" id="tagPaginationContainer" style="display: none;">
  754. <button id="tagPrevPageBtn" onclick="loadImageTags(currentTagPage - 1)" disabled>
  755. <i class="fas fa-chevron-left"></i> 上一页
  756. </button>
  757. <span id="tagPageInfo">第 1 页</span>
  758. <button id="tagNextPageBtn" onclick="loadImageTags(currentTagPage + 1)">
  759. 下一页 <i class="fas fa-chevron-right"></i>
  760. </button>
  761. </div>
  762. `;
  763. // 加载标签列表
  764. currentTagPage = 1;
  765. await loadImageTags(1);
  766. enhanceTagSearchContainer();
  767. } catch (error) {
  768. console.error('Error loading image details:', error);
  769. imageTagsView.innerHTML = `
  770. <div class="tag-header">
  771. <div class="tag-breadcrumb">
  772. <a href="javascript:void(0);" onclick="showSearchResults()">返回搜索结果</a>
  773. </div>
  774. <div class="error-message">
  775. <i class="fas fa-exclamation-circle"></i>
  776. <p>加载镜像详情失败: ${error.message}</p>
  777. <button onclick="viewImageDetails('${currentImageData.name.replace(/'/g, "\\'")}', ${currentImageData.isOfficial}, '${encodeURIComponent(currentImageData.description).replace(/'/g, "%27")}', ${currentImageData.stars}, ${currentImageData.pulls})" class="retry-btn">
  778. <button class="retry-btn">
  779. <i class="fas fa-redo"></i> 重试
  780. </button>
  781. </div>
  782. </div>
  783. `;
  784. showToastNotification(`加载镜像详情失败: ${error.message}`, 'error');
  785. }
  786. }
  787. window.viewImageDetails = viewImageDetails;
  788. // 新增: 加载所有标签 - 改进错误处理
  789. async function loadAllTags() {
  790. if (!currentImageData) {
  791. console.error('No image data available');
  792. return;
  793. }
  794. const loadAllTagsBtn = document.getElementById('loadAllTagsBtn');
  795. const tagsResults = document.getElementById('tagsResults');
  796. // 禁用按钮,显示加载状态
  797. loadAllTagsBtn.disabled = true;
  798. loadAllTagsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在加载全部TAG...';
  799. tagsResults.innerHTML = '<div class="loading-indicator">加载所有TAG中,这可能需要一些时间...</div>';
  800. try {
  801. // 先获取标签总数
  802. const countApiUrl = `/api/dockerhub/tag-count?name=${encodeURIComponent(currentImageData.name)}&official=${currentImageData.isOfficial}`;
  803. const countData = await fetchWithRetry(countApiUrl);
  804. const totalTags = countData.count || 0;
  805. if (totalTags === 0) {
  806. tagsResults.innerHTML = '<div class="message-container"><i class="fas fa-info-circle"></i><p>未找到标签信息</p></div>';
  807. showToastNotification(`该镜像没有可用的标签`, 'info');
  808. loadAllTagsBtn.disabled = false;
  809. loadAllTagsBtn.innerHTML = '<i class="fas fa-cloud-download-alt"></i> 加载全部TAG';
  810. return;
  811. }
  812. // 计算需要请求的次数 (每页最多100个标签)
  813. const pageSize = 100;
  814. const totalPages = Math.ceil(totalTags / pageSize);
  815. // 如果标签太多,提示用户
  816. if (totalTags > 3000) {
  817. const confirmLoad = confirm(`该镜像包含 ${totalTags} 个标签,加载全部可能会很慢。确定继续吗?`);
  818. if (!confirmLoad) {
  819. loadAllTagsBtn.disabled = false;
  820. loadAllTagsBtn.innerHTML = '<i class="fas fa-cloud-download-alt"></i> 加载全部TAG';
  821. tagsResults.innerHTML = '';
  822. await loadImageTags(1); // 加载第一页
  823. return;
  824. }
  825. }
  826. // 所有标签的集合
  827. let allTags = [];
  828. let loadedPages = 0;
  829. // 更新加载进度的函数
  830. const updateProgress = () => {
  831. loadAllTagsBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> 正在加载 (${Math.round((loadedPages/totalPages)*100)}%)`;
  832. tagsResults.innerHTML = `<div class="loading-indicator">已加载 ${allTags.length} / ${totalTags} 个标签 (${Math.round((loadedPages/totalPages)*100)}%)...</div>`;
  833. };
  834. // 分批加载所有标签
  835. for (let page = 1; page <= totalPages; page++) {
  836. try {
  837. const apiUrl = `/api/dockerhub/tags?name=${encodeURIComponent(currentImageData.name)}&official=${currentImageData.isOfficial}&page=${page}&page_size=${pageSize}`;
  838. // 使用新的fetchWithRetry函数
  839. const data = await fetchWithRetry(apiUrl);
  840. if (data.results && Array.isArray(data.results)) {
  841. // 处理标签中缺少平台信息的情况
  842. const processedTags = data.results.map(tag => {
  843. if (!tag.images || !Array.isArray(tag.images) || tag.images.length === 0) {
  844. tag.images = [];
  845. }
  846. return tag;
  847. });
  848. allTags = allTags.concat(processedTags);
  849. }
  850. loadedPages++;
  851. updateProgress();
  852. } catch (error) {
  853. console.error(`加载第 ${page} 页标签出错:`, error);
  854. }
  855. }
  856. if (allTags.length > 0) {
  857. // 为加载的所有标签实现客户端分页
  858. window.allLoadedTags = allTags; // 保存所有标签到全局变量
  859. window.currentAllTagsPage = 1;
  860. window.tagsPerPage = 25; // 修改: 每页显示25个标签而不是50个
  861. // 计算总页数
  862. const clientTotalPages = Math.ceil(allTags.length / window.tagsPerPage);
  863. // 显示第一页标签(这会自动创建分页控制器)
  864. displayAllTagsPage(1);
  865. showToastNotification(`成功加载 ${allTags.length} / ${totalTags} 个标签,分${clientTotalPages}页显示`, 'success');
  866. // 滚动到顶部
  867. window.scrollTo({
  868. top: document.getElementById('imageTagsView').offsetTop - 80,
  869. behavior: 'smooth'
  870. });
  871. } else {
  872. tagsResults.innerHTML = '<div class="message-container"><i class="fas fa-info-circle"></i><p>未找到标签信息</p></div>';
  873. showToastNotification(`未能加载标签`, 'info');
  874. }
  875. } catch (error) {
  876. console.error('加载全部标签失败:', error);
  877. tagsResults.innerHTML = `
  878. <div class="error-message">
  879. <i class="fas fa-exclamation-circle"></i>
  880. <p>加载全部标签失败: ${error.message}</p>
  881. <button onclick="loadImageTags(1)" class="retry-btn">
  882. <i class="fas fa-redo"></i> 返回常规模式
  883. </button>
  884. </div>
  885. `;
  886. showToastNotification(`加载全部标签失败: ${error.message}`, 'error');
  887. } finally {
  888. // 恢复按钮状态
  889. loadAllTagsBtn.disabled = false;
  890. loadAllTagsBtn.innerHTML = '<i class="fas fa-cloud-download-alt"></i> 加载全部TAG';
  891. }
  892. }
  893. window.loadAllTags = loadAllTags;
  894. // 添加 loadImageTags 函数定义
  895. async function loadImageTags(page = 1) {
  896. if (!currentImageData) {
  897. console.error('No image data available');
  898. return;
  899. }
  900. const tagsResults = document.getElementById('tagsResults');
  901. tagsResults.innerHTML = '<div class="loading-indicator">加载TAG列表中...</div>';
  902. try {
  903. // 构建API URL
  904. const apiUrl = `/api/dockerhub/tags?name=${encodeURIComponent(currentImageData.name)}&official=${currentImageData.isOfficial}&page=${page}&page_size=25`;
  905. // console.log('Requesting tags from:', apiUrl);
  906. // 使用fetchWithRetry获取数据
  907. const data = await fetchWithRetry(apiUrl);
  908. // console.log('Received tags data:', data);
  909. currentTagPage = page; // 更新当前页码
  910. if (data.results && data.results.length > 0) {
  911. // 处理标签中缺少平台信息的情况
  912. const processedTags = data.results.map(tag => {
  913. // 确保tag.images存在
  914. if (!tag.images || !Array.isArray(tag.images) || tag.images.length === 0) {
  915. tag.images = [];
  916. }
  917. return tag;
  918. });
  919. // 显示标签列表
  920. displayTags(processedTags);
  921. // 更新分页信息
  922. updateTagPagination(page, Math.ceil((data.count || 0) / 25));
  923. document.getElementById('tagPaginationContainer').style.display = 'flex';
  924. // 更新页面显示信息
  925. const tagStatsDiv = document.querySelector('.tag-search-stats');
  926. if (tagStatsDiv) {
  927. tagStatsDiv.innerHTML = `<p>共找到 <strong>${data.count || processedTags.length}</strong> 个标签,当前显示第 <strong>${(page-1)*25+1}</strong> 至 <strong>${Math.min(page*25, data.count)}</strong> 个</p>`;
  928. }
  929. } else {
  930. tagsResults.innerHTML = '<div class="message-container"><i class="fas fa-info-circle"></i><p>未找到标签信息</p></div>';
  931. document.getElementById('tagPaginationContainer').style.display = 'none';
  932. }
  933. } catch (error) {
  934. console.error('Error loading tags:', error);
  935. tagsResults.innerHTML = `
  936. <div class="error-message">
  937. <i class="fas fa-exclamation-circle"></i>
  938. <p>加载标签失败: ${error.message}</p>
  939. <button onclick="loadImageTags(${page})" class="retry-btn">
  940. <i class="fas fa-redo"></i> 重试
  941. </button>
  942. </div>
  943. `;
  944. document.getElementById('tagPaginationContainer').style.display = 'none';
  945. showToastNotification(`加载标签失败: ${error.message}`, 'error');
  946. }
  947. }
  948. window.loadImageTags = loadImageTags;
  949. // 新增: 显示客户端分页控制器
  950. function displayClientPagination(totalPages) {
  951. const tagsResults = document.getElementById('tagsResults');
  952. // 创建分页容器
  953. const paginationDiv = document.createElement('div');
  954. paginationDiv.className = 'pagination-container'; // 使用相同的样式类名
  955. paginationDiv.id = 'clientPaginationContainer';
  956. // 添加分页控制,格式与默认分页控制器相同
  957. paginationDiv.innerHTML = `
  958. <button id="clientPrevPageBtn" onclick="navigateAllTagsPage(-1)" disabled>
  959. <i class="fas fa-chevron-left"></i> 上一页
  960. </button>
  961. <span id="clientPageInfo">第 1 页 / 共 ${totalPages} 页</span>
  962. <button id="clientNextPageBtn" onclick="navigateAllTagsPage(1)" ${totalPages <= 1 ? 'disabled' : ''}>
  963. 下一页 <i class="fas fa-chevron-right"></i>
  964. </button>
  965. `;
  966. // 确保分页控制器添加到表格底部
  967. const existingPagination = document.getElementById('tagPaginationContainer');
  968. if (existingPagination && existingPagination.parentNode) {
  969. // 在原始分页控制器的位置插入新的分页控制器
  970. existingPagination.parentNode.insertBefore(paginationDiv, existingPagination);
  971. // 隐藏原来的分页控件
  972. existingPagination.style.display = 'none';
  973. } else {
  974. // 如果找不到原始分页控制器,添加到结果容器末尾
  975. tagsResults.appendChild(paginationDiv);
  976. }
  977. }
  978. // 新增: 切换到指定页面
  979. function displayAllTagsPage(page) {
  980. if (!window.allLoadedTags) return;
  981. const totalTags = window.allLoadedTags.length;
  982. // 修改: 将每页标签数量从50改为25
  983. window.tagsPerPage = 25; // 每页显示25个标签
  984. const tagsPerPage = window.tagsPerPage;
  985. const totalPages = Math.ceil(totalTags / tagsPerPage);
  986. // 确保页码在有效范围内
  987. if (page < 1) page = 1;
  988. if (page > totalPages) page = totalPages;
  989. window.currentAllTagsPage = page;
  990. // 计算当前页的标签
  991. const startIndex = (page - 1) * tagsPerPage;
  992. const endIndex = Math.min(startIndex + tagsPerPage, totalTags);
  993. const currentPageTags = window.allLoadedTags.slice(startIndex, endIndex);
  994. // 使用现有的displayTags函数显示当前页的标签
  995. displayTags(currentPageTags);
  996. enhanceTagSearchContainer();
  997. // 更新分页信息
  998. const pageInfo = document.getElementById('clientPageInfo');
  999. if (pageInfo) {
  1000. pageInfo.textContent = `第 ${page} 页 / 共 ${totalPages} 页`;
  1001. }
  1002. // 更新按钮状态
  1003. const prevBtn = document.getElementById('clientPrevPageBtn');
  1004. const nextBtn = document.getElementById('clientNextPageBtn');
  1005. if (prevBtn) prevBtn.disabled = page <= 1;
  1006. if (nextBtn) nextBtn.disabled = page >= totalPages;
  1007. // 更新标签统计信息
  1008. const tagStatsDiv = document.querySelector('.tag-search-stats');
  1009. if (tagStatsDiv) {
  1010. tagStatsDiv.innerHTML = `<p>显示 <strong>${startIndex + 1}-${endIndex}</strong> 个标签,共 <strong>${totalTags}</strong> 个</p>`;
  1011. }
  1012. // 创建新的客户端分页控制器
  1013. const clientPaginationContainer = document.getElementById('clientPaginationContainer');
  1014. if (!clientPaginationContainer) {
  1015. displayClientPagination(totalPages);
  1016. }
  1017. }
  1018. // 新增: 页面导航函数
  1019. function navigateAllTagsPage(direction) {
  1020. const newPage = window.currentAllTagsPage + direction;
  1021. displayAllTagsPage(newPage);
  1022. // 滚动到分页控制器位置,确保用户可以看到分页器
  1023. const paginationContainer = document.getElementById('clientPaginationContainer');
  1024. if (paginationContainer) {
  1025. paginationContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
  1026. }
  1027. }
  1028. // 显示TAG列表 - 改进默认排序和显示
  1029. function displayTags(tags) {
  1030. const tagsResults = document.getElementById('tagsResults');
  1031. tagsResults.innerHTML = '';
  1032. if (tags.length === 0) {
  1033. tagsResults.innerHTML = '<div class="message-container">没有找到匹配的TAG</div>';
  1034. return;
  1035. }
  1036. // 添加标签搜索统计信息
  1037. const searchStatsDiv = document.createElement('div');
  1038. searchStatsDiv.className = 'tag-search-stats';
  1039. searchStatsDiv.innerHTML = `<p>共找到 <strong>${tags.length}</strong> 个标签</p>`;
  1040. tagsResults.appendChild(searchStatsDiv);
  1041. // 添加标签排序功能
  1042. const sortContainer = document.createElement('div');
  1043. sortContainer.className = 'tag-sort-container';
  1044. sortContainer.innerHTML = `
  1045. <label for="tagSort">排序方式:</label>
  1046. <select id="tagSort" onchange="sortTags()">
  1047. <option value="name-asc">TAG名称 (A-Z)</option>
  1048. <option value="name-desc">TAG名称 (Z-A)</option>
  1049. <option value="date-desc" selected>最新更新</option>
  1050. <option value="date-asc">最早更新</option>
  1051. <option value="size-desc">大小 (大-小)</option>
  1052. <option value="size-asc">大小 (小-大)</option>
  1053. </select>
  1054. `;
  1055. tagsResults.appendChild(sortContainer);
  1056. // 创建表格容器以启用水平滚动
  1057. const tableContainer = document.createElement('div');
  1058. tableContainer.className = 'tag-table-container';
  1059. tagsResults.appendChild(tableContainer);
  1060. const tagTable = document.createElement('table');
  1061. tagTable.className = 'tag-table';
  1062. tagTable.id = 'tagTable';
  1063. const thead = document.createElement('thead');
  1064. thead.innerHTML = `
  1065. <tr>
  1066. <th width="18%">TAG</th>
  1067. <th width="42%">OS/ARCH</th>
  1068. <th width="15%">大小</th>
  1069. <th width="15%">更新时间</th>
  1070. <th width="10%">操作</th>
  1071. </tr>
  1072. `;
  1073. tagTable.appendChild(thead);
  1074. const tbody = document.createElement('tbody');
  1075. tbody.id = 'tagTableBody';
  1076. // 使用最新更新的默认排序
  1077. window.currentTags = [...tags];
  1078. sortTagsByDate('desc');
  1079. renderTagRows(window.currentTags, tbody);
  1080. tagTable.appendChild(tbody);
  1081. tableContainer.appendChild(tagTable); // 将表格添加到容器中
  1082. // 添加调试信息
  1083. // console.log(`显示了 ${tags.length} 个标签`);
  1084. }
  1085. // 新增的排序标签函数
  1086. function sortTags() {
  1087. const sortSelect = document.getElementById('tagSort');
  1088. const [sortBy, direction] = sortSelect.value.split('-');
  1089. if (sortBy === 'name') {
  1090. sortTagsByName(direction);
  1091. } else if (sortBy === 'date') {
  1092. sortTagsByDate(direction);
  1093. } else if (sortBy === 'size') {
  1094. sortTagsBySize(direction);
  1095. }
  1096. const tbody = document.getElementById('tagTableBody');
  1097. tbody.innerHTML = '';
  1098. renderTagRows(window.currentTags, tbody);
  1099. }
  1100. // 按名称排序
  1101. function sortTagsByName(direction) {
  1102. window.currentTags.sort((a, b) => {
  1103. return direction === 'asc'
  1104. ? a.name.localeCompare(b.name)
  1105. : b.name.localeCompare(a.name);
  1106. });
  1107. }
  1108. // 按日期排序
  1109. function sortTagsByDate(direction) {
  1110. window.currentTags.sort((a, b) => {
  1111. const dateA = a.last_updated ? new Date(a.last_updated) : new Date(0);
  1112. const dateB = b.last_updated ? new Date(b.last_updated) : new Date(0);
  1113. return direction === 'asc' ? dateA - dateB : dateB - dateA;
  1114. });
  1115. }
  1116. // 按大小排序
  1117. function sortTagsBySize(direction) {
  1118. window.currentTags.sort((a, b) => {
  1119. const sizeA = a.full_size || 0;
  1120. const sizeB = b.full_size || 0;
  1121. return direction === 'asc' ? sizeA - sizeB : sizeB - sizeA;
  1122. });
  1123. }
  1124. // 渲染标签行
  1125. function renderTagRows(tags, tbody) {
  1126. tags.forEach((tag, index) => {
  1127. const tr = document.createElement('tr');
  1128. // 计算大小
  1129. let size = '未知';
  1130. if (tag.full_size) {
  1131. const sizeInMB = Math.round(tag.full_size / 1024 / 1024);
  1132. size = `${sizeInMB} MB`;
  1133. }
  1134. // 格式化日期
  1135. let lastUpdated = '未知';
  1136. if (tag.last_updated) {
  1137. const date = new Date(tag.last_updated);
  1138. lastUpdated = date.toLocaleDateString('zh-CN');
  1139. }
  1140. tr.innerHTML = `
  1141. <td>${tag.name}</td>
  1142. <td>${createOsArchHtml(tag.images, index)}</td>
  1143. <td>${size}</td>
  1144. <td>${lastUpdated}</td>
  1145. <td>
  1146. <button class="primary-btn" onclick="useImage('${currentImageData.name}:${tag.name}')">
  1147. <i class="fas fa-rocket"></i> 使用
  1148. </button>
  1149. </td>
  1150. `;
  1151. tbody.appendChild(tr);
  1152. });
  1153. }
  1154. function createOsArchHtml(images, tagIndex) {
  1155. // 确保images是有效数据
  1156. if (!images || !Array.isArray(images) || images.length === 0) {
  1157. return '<div class="tag-os-arch"><span class="tag-os-arch-item">无平台信息</span></div>';
  1158. }
  1159. // 过滤和去重平台信息,过滤掉unknown/unknown
  1160. const uniquePlatforms = [];
  1161. const seen = new Set();
  1162. images.forEach(img => {
  1163. if (img && img.os && img.architecture) {
  1164. // 跳过unknown/unknown组合
  1165. if (img.os === 'unknown' && img.architecture === 'unknown') {
  1166. return;
  1167. }
  1168. const key = `${img.os}/${img.architecture}${img.variant ? '/' + img.variant : ''}`;
  1169. if (!seen.has(key)) {
  1170. seen.add(key);
  1171. uniquePlatforms.push(img);
  1172. }
  1173. }
  1174. });
  1175. if (uniquePlatforms.length === 0) {
  1176. return '<div class="tag-os-arch"><span class="tag-os-arch-item">无平台信息</span></div>';
  1177. }
  1178. // 改进的显示逻辑:以列表形式显示所有平台
  1179. const mainPlatforms = uniquePlatforms.slice(0, 4); // 显示前4个
  1180. const extraPlatforms = uniquePlatforms.slice(4); // 其余隐藏
  1181. let html = '<div class="tag-os-arch">';
  1182. // 显示主要平台
  1183. mainPlatforms.forEach(img => {
  1184. html += `<span class="tag-os-arch-item">${img.os}/${img.architecture}${img.variant ? '/' + img.variant : ''}</span>`;
  1185. });
  1186. // 如果有更多平台,添加展开功能
  1187. if (extraPlatforms.length > 0) {
  1188. html += `
  1189. <span class="tag-os-arch-more" onclick="toggleOsArch(${tagIndex})">
  1190. <i class="fas fa-plus-circle"></i> 显示更多(${extraPlatforms.length})
  1191. </span>
  1192. <div id="osArch${tagIndex}" class="tag-os-arch-all">
  1193. `;
  1194. extraPlatforms.forEach(img => {
  1195. html += `<span class="tag-os-arch-item">${img.os}/${img.architecture}${img.variant ? '/' + img.variant : ''}</span>`;
  1196. });
  1197. html += '</div>';
  1198. }
  1199. html += '</div>';
  1200. return html;
  1201. }
  1202. function toggleOsArch(tagIndex) {
  1203. const element = document.getElementById(`osArch${tagIndex}`);
  1204. element.classList.toggle('show');
  1205. const moreBtn = element.previousElementSibling;
  1206. if (element.classList.contains('show')) {
  1207. moreBtn.innerHTML = '<i class="fas fa-minus-circle"></i> 收起';
  1208. } else {
  1209. moreBtn.innerHTML = `<i class="fas fa-plus-circle"></i> 显示更多(${element.children.length})`;
  1210. }
  1211. }
  1212. // 修改TAG过滤功能 - 支持搜索所有已加载的标签
  1213. function filterTags() {
  1214. const searchTerm = document.getElementById('tagSearchInput').value.toLowerCase().trim();
  1215. // 检查是否已加载全部标签
  1216. if (window.allLoadedTags && searchTerm) {
  1217. // 在所有加载的标签中搜索
  1218. const matchedTags = window.allLoadedTags.filter(tag =>
  1219. tag.name.toLowerCase().includes(searchTerm)
  1220. );
  1221. // 更新搜索统计信息
  1222. const searchStatsDiv = document.querySelector('.tag-search-stats');
  1223. if (searchStatsDiv) {
  1224. searchStatsDiv.innerHTML = `<p>过滤结果: 共找到 <strong>${matchedTags.length}</strong> 个匹配 "${searchTerm}" 的标签 (共${window.allLoadedTags.length}个)</p>`;
  1225. }
  1226. // 如果有匹配的标签
  1227. if (matchedTags.length > 0) {
  1228. // 显示匹配的标签
  1229. displayTags(matchedTags);
  1230. // 隐藏分页控件,显示所有匹配结果
  1231. const clientPagination = document.getElementById('clientPaginationContainer');
  1232. if (clientPagination) {
  1233. clientPagination.style.display = 'none';
  1234. }
  1235. } else {
  1236. // 无匹配结果提示
  1237. const tagsResults = document.getElementById('tagsResults');
  1238. // 保留搜索统计信息
  1239. const statsHTML = tagsResults.innerHTML.split('</div>')[0] + '</div>';
  1240. tagsResults.innerHTML = statsHTML + '<div class="no-filter-results"><p>没有匹配 "' + searchTerm + '" 的标签</p></div>';
  1241. }
  1242. return; // 已处理全局搜索,不继续执行
  1243. }
  1244. // 原有的过滤逻辑 - 只搜索当前页面上的标签
  1245. const rows = document.querySelectorAll('.tag-table tbody tr');
  1246. if (!rows.length) return;
  1247. let visibleCount = 0;
  1248. rows.forEach(row => {
  1249. const tagName = row.querySelector('td:first-child').textContent.toLowerCase();
  1250. if (tagName.includes(searchTerm)) {
  1251. row.style.display = '';
  1252. visibleCount++;
  1253. } else {
  1254. row.style.display = 'none';
  1255. }
  1256. });
  1257. // 更新过滤后的统计信息
  1258. const searchStatsDiv = document.querySelector('.tag-search-stats');
  1259. if (searchStatsDiv) {
  1260. if (searchTerm) {
  1261. searchStatsDiv.innerHTML = `<p>过滤结果: 共找到 <strong>${visibleCount}</strong> 个匹配 "${searchTerm}" 的标签</p>`;
  1262. } else {
  1263. searchStatsDiv.innerHTML = `<p>共找到 <strong>${rows.length}</strong> 个标签</p>`;
  1264. }
  1265. }
  1266. // 如果没有匹配的结果,显示提示
  1267. const tagsResults = document.getElementById('tagsResults');
  1268. const noResultsEl = tagsResults.querySelector('.no-filter-results');
  1269. if (visibleCount === 0 && searchTerm) {
  1270. if (!noResultsEl) {
  1271. const message = document.createElement('div');
  1272. message.className = 'no-filter-results';
  1273. message.innerHTML = `<p>没有匹配 "${searchTerm}" 的TAG</p>`;
  1274. tagsResults.appendChild(message);
  1275. }
  1276. } else if (noResultsEl) {
  1277. noResultsEl.remove();
  1278. }
  1279. }
  1280. window.filterTags = filterTags;
  1281. // 添加重置搜索功能
  1282. function resetTagSearch() {
  1283. const searchInput = document.getElementById('tagSearchInput');
  1284. if (searchInput) {
  1285. searchInput.value = '';
  1286. }
  1287. // 如果已加载全部标签,重新显示当前页
  1288. if (window.allLoadedTags) {
  1289. displayAllTagsPage(window.currentAllTagsPage || 1);
  1290. // 恢复分页控件显示
  1291. const clientPagination = document.getElementById('clientPaginationContainer');
  1292. if (clientPagination) {
  1293. clientPagination.style.display = 'flex';
  1294. }
  1295. } else {
  1296. // 否则重新加载当前标签页
  1297. loadImageTags(currentTagPage);
  1298. }
  1299. }
  1300. window.resetTagSearch = resetTagSearch;
  1301. // 修改标签搜索容器,添加重置按钮
  1302. function enhanceTagSearchContainer() {
  1303. const container = document.querySelector('.tag-search-container');
  1304. if (container) {
  1305. // 检查是否已经增强过
  1306. if (!container.querySelector('.reset-btn')) {
  1307. // 添加重置按钮
  1308. const resetBtn = document.createElement('button');
  1309. resetBtn.className = 'reset-btn';
  1310. resetBtn.innerHTML = '<i class="fas fa-times"></i> 重置';
  1311. resetBtn.onclick = resetTagSearch;
  1312. container.appendChild(resetBtn);
  1313. // 修改搜索按钮点击事件
  1314. const searchBtn = container.querySelector('.search-btn');
  1315. if (searchBtn) {
  1316. searchBtn.onclick = filterTags;
  1317. }
  1318. }
  1319. }
  1320. }
  1321. // 显示指定的文档
  1322. function showDocument(index) {
  1323. // 清理之前的返回顶部按钮
  1324. const existingBackToTopBtn = document.querySelector('.back-to-top-btn');
  1325. if (existingBackToTopBtn) {
  1326. existingBackToTopBtn.remove();
  1327. }
  1328. if (!window.documentationData || !Array.isArray(window.documentationData)) {
  1329. console.error('文档数据不可用');
  1330. return;
  1331. }
  1332. // 处理数字索引或字符串ID
  1333. let docIndex = index;
  1334. let doc = null;
  1335. if (typeof index === 'string') {
  1336. // 如果是ID,找到对应的索引
  1337. docIndex = window.documentationData.findIndex(doc =>
  1338. (doc.id === index || doc._id === index)
  1339. );
  1340. if (docIndex === -1) {
  1341. console.error('找不到ID为', index, '的文档');
  1342. return;
  1343. }
  1344. }
  1345. doc = window.documentationData[docIndex];
  1346. if (!doc) {
  1347. console.error('指定索引的文档不存在:', docIndex);
  1348. return;
  1349. }
  1350. // console.log('文档数据:', doc);
  1351. // 高亮选中的文档
  1352. const docLinks = document.querySelectorAll('.doc-list li a');
  1353. docLinks.forEach((link, i) => {
  1354. if (i === docIndex) {
  1355. link.classList.add('active');
  1356. } else {
  1357. link.classList.remove('active');
  1358. }
  1359. });
  1360. const docContent = document.getElementById('documentationText');
  1361. if (!docContent) {
  1362. console.error('找不到文档内容容器');
  1363. return;
  1364. }
  1365. // 显示加载状态
  1366. docContent.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 正在加载文档内容...</div>';
  1367. // 如果文档内容不存在,则需要获取完整内容
  1368. if (!doc.content) {
  1369. const docId = doc.id || doc._id;
  1370. // console.log('获取文档内容,ID:', docId);
  1371. fetch(`/api/documentation/${docId}`)
  1372. .then(response => {
  1373. // console.log('文档API响应:', response.status, response.statusText);
  1374. if (!response.ok) {
  1375. throw new Error(`获取文档内容失败: ${response.status}`);
  1376. }
  1377. return response.json();
  1378. })
  1379. .then(fullDoc => {
  1380. // console.log('获取到完整文档:', fullDoc);
  1381. // 更新缓存的文档内容
  1382. window.documentationData[docIndex].content = fullDoc.content;
  1383. // 渲染文档内容
  1384. renderDocumentContent(docContent, fullDoc);
  1385. })
  1386. .catch(error => {
  1387. console.error('获取文档内容失败:', error);
  1388. docContent.innerHTML = `
  1389. <div class="error-container">
  1390. <i class="fas fa-exclamation-triangle fa-3x"></i>
  1391. <h2>加载失败</h2>
  1392. <p>无法获取文档内容: ${error.message}</p>
  1393. </div>
  1394. `;
  1395. });
  1396. } else {
  1397. // 直接渲染已有的文档内容
  1398. renderDocumentContent(docContent, doc);
  1399. }
  1400. }
  1401. window.showDocument = showDocument;
  1402. // 渲染文档内容
  1403. function renderDocumentContent(container, doc) {
  1404. if (!container) return;
  1405. // console.log('正在渲染文档:', doc);
  1406. // 确保有内容可渲染
  1407. if (!doc.content && !doc.path) {
  1408. container.innerHTML = `
  1409. <h1>${doc.title || '未知文档'}</h1>
  1410. <div class="empty-content">
  1411. <i class="fas fa-file-alt fa-3x"></i>
  1412. <p>该文档暂无内容</p>
  1413. </div>
  1414. `;
  1415. return;
  1416. }
  1417. // 根据文档内容类型进行渲染
  1418. if (doc.content) {
  1419. renderMarkdownContent(container, doc);
  1420. } else {
  1421. // 如果是文件路径但无内容,尝试获取
  1422. fetch(`/api/documentation/file?path=${encodeURIComponent(doc.id + '.md')}`)
  1423. .then(response => {
  1424. // console.log('文件内容响应:', response.status, response.statusText);
  1425. if (!response.ok) {
  1426. throw new Error(`获取文件内容失败: ${response.status}`);
  1427. }
  1428. return response.text();
  1429. })
  1430. .then(content => {
  1431. // console.log('获取到文件内容,长度:', content.length);
  1432. doc.content = content;
  1433. renderMarkdownContent(container, doc);
  1434. })
  1435. .catch(error => {
  1436. console.error('获取文件内容失败:', error);
  1437. container.innerHTML = `
  1438. <div class="error-container">
  1439. <i class="fas fa-exclamation-triangle fa-3x"></i>
  1440. <h2>加载失败</h2>
  1441. <p>无法获取文档内容: ${error.message}</p>
  1442. </div>
  1443. `;
  1444. });
  1445. }
  1446. }
  1447. // 渲染Markdown内容
  1448. function renderMarkdownContent(container, doc) {
  1449. if (!container) return;
  1450. if (doc.content) {
  1451. // 使用marked渲染Markdown内容
  1452. if (window.marked) {
  1453. try {
  1454. // 配置marked选项以获得更好的渲染效果
  1455. marked.setOptions({
  1456. highlight: function(code, lang) {
  1457. // 如果有语法高亮库,可以在这里使用
  1458. return code;
  1459. },
  1460. langPrefix: 'language-',
  1461. breaks: true,
  1462. gfm: true
  1463. });
  1464. const rawHtml = marked.parse(doc.content);
  1465. // 创建一个临时的根元素来容纳和处理已解析的Markdown内容
  1466. const docFragmentRoot = document.createElement('div');
  1467. docFragmentRoot.innerHTML = rawHtml;
  1468. // 为代码块添加语言标识和复制按钮
  1469. const preElements = docFragmentRoot.querySelectorAll('pre');
  1470. preElements.forEach((preElement, index) => {
  1471. const codeElement = preElement.querySelector('code');
  1472. let codeToCopy = '';
  1473. let language = 'Code';
  1474. if (codeElement) {
  1475. codeToCopy = codeElement.textContent;
  1476. // 尝试从className获取语言信息
  1477. const className = codeElement.className;
  1478. const langMatch = className.match(/language-(\w+)/);
  1479. if (langMatch) {
  1480. language = langMatch[1].toUpperCase();
  1481. }
  1482. } else {
  1483. codeToCopy = preElement.textContent;
  1484. }
  1485. // 设置语言属性用于CSS显示
  1486. preElement.setAttribute('data-language', language);
  1487. if (codeToCopy.trim() !== '') {
  1488. const copyButton = document.createElement('button');
  1489. copyButton.className = 'copy-btn';
  1490. copyButton.innerHTML = '<i class="fas fa-copy"></i> 复制';
  1491. copyButton.onclick = function() {
  1492. copyToClipboard(codeToCopy, this);
  1493. };
  1494. preElement.appendChild(copyButton);
  1495. }
  1496. });
  1497. // 为链接添加外部链接图标
  1498. const links = docFragmentRoot.querySelectorAll('a');
  1499. links.forEach(link => {
  1500. const href = link.getAttribute('href');
  1501. if (href && (href.startsWith('http') || href.startsWith('https'))) {
  1502. link.innerHTML += ' <i class="fas fa-external-link-alt" style="font-size: 0.8em; margin-left: 0.25rem;"></i>';
  1503. link.setAttribute('target', '_blank');
  1504. link.setAttribute('rel', 'noopener noreferrer');
  1505. }
  1506. });
  1507. // 为表格添加响应式包装
  1508. const tables = docFragmentRoot.querySelectorAll('table');
  1509. tables.forEach(table => {
  1510. const wrapper = document.createElement('div');
  1511. wrapper.className = 'table-wrapper';
  1512. wrapper.style.overflowX = 'auto';
  1513. wrapper.style.marginBottom = '1.5rem';
  1514. table.parentNode.insertBefore(wrapper, table);
  1515. wrapper.appendChild(table);
  1516. });
  1517. // 清空页面上的主容器
  1518. container.innerHTML = '';
  1519. // 创建文档头部
  1520. const docHeader = document.createElement('div');
  1521. docHeader.className = 'doc-header';
  1522. docHeader.innerHTML = `
  1523. <h1>${doc.title || '文档标题'}</h1>
  1524. ${doc.description ? `<p class="doc-description">${doc.description}</p>` : ''}
  1525. `;
  1526. container.appendChild(docHeader);
  1527. // 创建 .doc-content div 并将处理过的文档片段追加进去
  1528. const docContentDiv = document.createElement('div');
  1529. docContentDiv.className = 'doc-content';
  1530. // 将 docFragmentRoot 的所有子节点移动到 docContentDiv
  1531. while (docFragmentRoot.firstChild) {
  1532. docContentDiv.appendChild(docFragmentRoot.firstChild);
  1533. }
  1534. container.appendChild(docContentDiv);
  1535. // 创建并追加 .doc-meta div
  1536. const docMetaDiv = document.createElement('div');
  1537. docMetaDiv.className = 'doc-meta';
  1538. const updateTime = doc.lastUpdated || doc.updatedAt || doc.updated_at;
  1539. if (updateTime) {
  1540. const formattedDate = new Date(updateTime).toLocaleDateString('zh-CN', {
  1541. year: 'numeric',
  1542. month: 'long',
  1543. day: 'numeric',
  1544. hour: '2-digit',
  1545. minute: '2-digit'
  1546. });
  1547. docMetaDiv.innerHTML = `
  1548. <i class="fas fa-clock"></i>
  1549. <span>最后更新: ${formattedDate}</span>
  1550. `;
  1551. }
  1552. container.appendChild(docMetaDiv);
  1553. // 添加返回顶部按钮(如果内容很长)
  1554. if (docContentDiv.scrollHeight > 1000) {
  1555. const backToTopBtn = document.createElement('button');
  1556. backToTopBtn.className = 'back-to-top-btn';
  1557. backToTopBtn.innerHTML = '<i class="fas fa-arrow-up"></i>';
  1558. backToTopBtn.style.cssText = `
  1559. position: fixed;
  1560. bottom: 2rem;
  1561. right: 2rem;
  1562. width: 3rem;
  1563. height: 3rem;
  1564. border-radius: 50%;
  1565. background: var(--primary-color);
  1566. color: white;
  1567. border: none;
  1568. cursor: pointer;
  1569. box-shadow: 0 4px 12px rgba(61, 124, 244, 0.3);
  1570. z-index: 1000;
  1571. opacity: 0.8;
  1572. transition: all 0.3s ease;
  1573. `;
  1574. backToTopBtn.onclick = () => {
  1575. container.scrollIntoView({ behavior: 'smooth' });
  1576. };
  1577. backToTopBtn.onmouseenter = () => {
  1578. backToTopBtn.style.opacity = '1';
  1579. backToTopBtn.style.transform = 'scale(1.1)';
  1580. };
  1581. backToTopBtn.onmouseleave = () => {
  1582. backToTopBtn.style.opacity = '0.8';
  1583. backToTopBtn.style.transform = 'scale(1)';
  1584. };
  1585. document.body.appendChild(backToTopBtn);
  1586. // 当切换文档时清理按钮
  1587. container.setAttribute('data-back-to-top', 'true');
  1588. }
  1589. } catch (error) {
  1590. console.error('Markdown解析失败:', error);
  1591. // 发生错误时的降级处理
  1592. container.innerHTML = `
  1593. <div class="doc-header">
  1594. <h1>${doc.title || '文档标题'}</h1>
  1595. </div>
  1596. <div class="doc-content">
  1597. <div class="error-container">
  1598. <i class="fas fa-exclamation-triangle"></i>
  1599. <h3>内容解析失败</h3>
  1600. <p>无法正确解析文档内容,显示原始内容:</p>
  1601. <pre><code>${doc.content}</code></pre>
  1602. </div>
  1603. </div>
  1604. <div class="doc-meta">
  1605. ${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
  1606. </div>
  1607. `;
  1608. }
  1609. } else {
  1610. // marked 不可用时的降级处理
  1611. container.innerHTML = `
  1612. <div class="doc-header">
  1613. <h1>${doc.title || '文档标题'}</h1>
  1614. </div>
  1615. <div class="doc-content">
  1616. <div class="markdown-fallback">
  1617. <p><em>Markdown 解析器未加载,显示原始内容:</em></p>
  1618. <pre><code>${doc.content}</code></pre>
  1619. </div>
  1620. </div>
  1621. <div class="doc-meta">
  1622. ${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
  1623. </div>
  1624. `;
  1625. }
  1626. } else {
  1627. // 文档无内容时,显示占位符
  1628. container.innerHTML = `
  1629. <div class="doc-content">
  1630. <div class="empty-content">
  1631. <i class="fas fa-file-alt fa-3x"></i>
  1632. <p>该文档暂无内容</p>
  1633. </div>
  1634. </div>
  1635. <div class="doc-meta">
  1636. <span>文档信息不可用</span>
  1637. </div>
  1638. `;
  1639. }
  1640. }
  1641. // 加载菜单
  1642. loadMenu();
  1643. // DOMContentLoaded 事件监听器
  1644. document.addEventListener('DOMContentLoaded', function() {
  1645. // 加载系统配置(包括 logo)
  1646. loadSystemConfig();
  1647. // 初始化代理域名
  1648. initProxyDomain();
  1649. // 确保元素存在再添加事件监听器
  1650. const searchInput = document.getElementById('searchInput');
  1651. if (searchInput) {
  1652. searchInput.addEventListener('keypress', function(event) {
  1653. if (event.key === 'Enter') {
  1654. searchDockerHub(1);
  1655. }
  1656. });
  1657. }
  1658. // 加载菜单
  1659. loadMenu();
  1660. // 统一调用文档加载函数
  1661. loadAndDisplayDocumentation();
  1662. });
  1663. // 加载系统配置
  1664. function loadSystemConfig() {
  1665. fetch('/api/config')
  1666. .then(response => {
  1667. if (response.ok) {
  1668. return response.json();
  1669. }
  1670. // 如果配置加载失败,使用默认配置
  1671. return {};
  1672. })
  1673. .then(config => {
  1674. const logoElement = document.getElementById('mainLogo');
  1675. if (logoElement) {
  1676. // 如果有自定义logo配置且不为空,则使用自定义logo
  1677. if (config.logo && config.logo.trim() !== '') {
  1678. logoElement.src = config.logo;
  1679. }
  1680. // 如果没有配置或为空,保持默认logo不变
  1681. // 显示logo(无论是默认还是自定义)
  1682. logoElement.style.opacity = '1';
  1683. }
  1684. })
  1685. .catch(error => {
  1686. // 如果出错,也要显示默认logo
  1687. console.warn('加载配置失败:', error);
  1688. const logoElement = document.getElementById('mainLogo');
  1689. if (logoElement) {
  1690. logoElement.style.opacity = '1';
  1691. }
  1692. });
  1693. }
  1694. </script>
  1695. <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/2.0.3/marked.min.js"></script>
  1696. </body>
  1697. </html>