From fba76bd11737ede12a217c4df08a8bdc1a10871d Mon Sep 17 00:00:00 2001 From: user123456 Date: Thu, 12 Jun 2025 14:05:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=A0=87=E7=AD=BE=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/public/search.html | 453 ++++++++++++++++++++++++++++------------- 1 file changed, 309 insertions(+), 144 deletions(-) diff --git a/src/public/search.html b/src/public/search.html index e6a3e2c..0c30d3a 100644 --- a/src/public/search.html +++ b/src/public/search.html @@ -451,12 +451,37 @@ transform: translateY(0); } - .tag-search-container { + /* 标签头部 */ + .tag-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 15px; + padding: 20px; background-color: var(--card); - border: 1px solid var(--border); border-radius: 0.75rem; - padding: 1.5rem; - margin-bottom: 1.5rem; + border: 1px solid var(--border); + flex-direction: column; + } + + .tag-search-container { + width: 100%; + margin: 15px 0 20px 0; + position: relative; + background: var(--card); + padding: 15px; + border-radius: 0.75rem; + border: 2px solid var(--primary); + box-shadow: 0 2px 8px rgba(37, 99, 235, 0.1); + } + + .tag-search-container::before { + content: '🔍 标签搜索'; + display: block; + margin-bottom: 10px; + color: var(--primary); + font-weight: bold; + font-size: 0.9rem; } .tag-search-header { @@ -470,19 +495,51 @@ .tag-search-input { width: 100%; - padding: 0.75rem 1rem; + padding: 12px 40px 12px 15px; + border-radius: 8px; border: 1px solid var(--border); - border-radius: var(--radius); background-color: var(--input); color: var(--foreground); font-size: 1rem; - transition: all 0.2s; + transition: all 0.3s ease; } .tag-search-input:focus { outline: none; - border-color: var(--ring); - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); + } + + .tag-search-input::placeholder { + color: #888; + font-style: italic; + } + + .tag-search-clear { + position: absolute; + right: 25px; + top: 50%; + transform: translateY(-5%); + background: none; + border: none; + color: var(--foreground); + cursor: pointer; + padding: 8px; + opacity: 0.6; + font-size: 1.2rem; + transition: all 0.3s ease; + border-radius: 50%; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + } + + .tag-search-clear:hover { + opacity: 1; + background-color: rgba(37, 99, 235, 0.1); + transform: translateY(-5%) scale(1.1); } .tag-info { @@ -573,9 +630,36 @@ .arch-item { background-color: var(--muted); color: var(--muted-foreground); - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - font-size: 0.75rem; + padding: 5px 10px; + border-radius: 5px; + font-size: 0.8rem; + } + + /* 漏洞指示器 */ + .vulnerability-indicator { + display: inline-flex; + gap: 5px; + margin-left: 10px; + } + + .vulnerability-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + } + + .vulnerability-critical { background-color: #dc3545; } + .vulnerability-high { background-color: #fd7e14; } + .vulnerability-medium { background-color: #ffc107; } + .vulnerability-low { background-color: #28a745; } + .vulnerability-unknown { background-color: #6c757d; } + + /* 组织标签 */ + .badge-organization { + background-color: #6c757d; + color: white; + font-weight: normal; } /* 返回按钮 */ @@ -740,24 +824,7 @@
- -
-
- 🔍 标签搜索 -
- -
- - -
- - -
+
@@ -804,29 +871,73 @@ prevPage: document.getElementById('prevPage'), nextPage: document.getElementById('nextPage'), tagList: document.getElementById('tagList'), - tagInfo: document.getElementById('tagInfo'), - tagItems: document.getElementById('tagItems'), - tagSearchInput: document.getElementById('tagSearchInput'), backToSearch: document.getElementById('backToSearch'), toast: document.getElementById('toast') }; - // 格式化工具 + // 格式化工具(按照原版逻辑) const formatUtils = { formatNumber(num) { - if (num >= 1000000000) return (num / 1000000000).toFixed(1) + 'B'; - if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; - if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; + if (num >= 1000000000) return (num / 1000000000).toFixed(1) + 'B+'; + if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M+'; + if (num >= 1000) return (num / 1000).toFixed(1) + 'K+'; return num.toString(); }, - formatSize(bytes) { - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - if (bytes === 0) return '0 B'; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; + formatTimeAgo(dateString) { + if (!dateString) return '未知时间'; + try { + let date; + date = new Date(dateString); + if (isNaN(date.getTime())) { + const formats = [ + { regex: /^(\d{4})-(\d{2})-(\d{2})$/, handler: (m) => new Date(m[1], m[2] - 1, m[3]) }, + { regex: /^(\d{4})\/(\d{2})\/(\d{2})$/, handler: (m) => new Date(m[1], m[2] - 1, m[3]) }, + { regex: /^(\d{2})\/(\d{2})\/(\d{4})$/, handler: (m) => new Date(m[3], m[1] - 1, m[2]) } + ]; + + for (const format of formats) { + const match = dateString.match(format.regex); + if (match) { + date = format.handler(match); + if (!isNaN(date.getTime())) break; + } + } + + if (isNaN(date.getTime())) { + return '未知时间'; + } + } + + const now = new Date(); + const diffTime = Math.abs(now - date); + const diffMinutes = Math.floor(diffTime / (1000 * 60)); + const diffHours = Math.floor(diffTime / (1000 * 60 * 60)); + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + const diffMonths = Math.floor(diffDays / 30); + const diffYears = Math.floor(diffDays / 365); + + if (diffMinutes < 1) return '刚刚'; + if (diffMinutes < 60) return `${diffMinutes}分钟前`; + if (diffHours < 24) return `${diffHours}小时前`; + if (diffDays < 7) return `${diffDays}天前`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)}周前`; + if (diffMonths < 12) return `${diffMonths}个月前`; + if (diffYears < 1) return '近1年'; + return `${diffYears}年前`; + } catch (error) { + console.warn('日期处理错误:', error); + return '未知时间'; + } }, - formatDate(dateString) { - return new Date(dateString).toLocaleDateString('zh-CN'); + formatSize(bytes) { + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + return `${size.toFixed(2)} ${units[unitIndex]}`; } }; @@ -895,25 +1006,18 @@ const card = document.createElement('div'); card.className = 'result-card'; - // 获取原始镜像名称 - const originalName = result.repo_name || result.name; - - // 确保只有真正的官方镜像才处理显示名称 - // 使用更严格的条件:必须同时满足 is_official 为 true 且名称以 library/ 开头 - const isActuallyOfficial = result.is_official === true && originalName.startsWith('library/'); - const displayName = isActuallyOfficial ? originalName.substring(8) : originalName; - - // 调试日志(可在开发时启用) - if (originalName.toLowerCase().includes('caddy')) { - console.log('Debug caddy:', { - originalName: originalName, - is_official: result.is_official, - isActuallyOfficial: isActuallyOfficial, - displayName: displayName - }); + // 按照原版逻辑构建显示名称 + let displayName = ''; + if (result.is_official) { + // 对于官方镜像,去掉 library/ 前缀 + displayName = (result.name || result.repo_name || '').replace('library/', ''); + } else { + // 对于非官方镜像,显示完整路径 + const name = result.name || result.repo_name || ''; + displayName = result.namespace ? `${result.namespace}/${name}` : name; } - card.onclick = () => viewImageTags(originalName, isActuallyOfficial); + card.onclick = () => viewImageTags(result, displayName); const badges = []; if (result.is_official) badges.push('官方'); @@ -945,7 +1049,7 @@ } // 查看镜像标签 - async function viewImageTags(imageName, isOfficial = false) { + async function viewImageTags(result, displayName) { if (isLoading) return; isLoading = true; @@ -953,17 +1057,14 @@ elements.searchResults.classList.remove('show'); try { - // 解析镜像名称 - let namespace = 'library'; - let repoName = imageName; + // 获取命名空间和仓库名 + const namespace = result.namespace || (result.is_official ? 'library' : ''); + const name = result.name || result.repo_name || ''; + const repoName = name.replace('library/', ''); - if (imageName.includes('/')) { - const parts = imageName.split('/'); - namespace = parts[0]; - repoName = parts[1]; - } else { - // 官方镜像 - namespace = 'library'; + if (!namespace || !repoName) { + showToast('无效的仓库信息', 'error'); + return; } const response = await fetch(`/tags/${namespace}/${repoName}`); @@ -971,7 +1072,7 @@ if (Array.isArray(data)) { allTags = data; - displayImageTags(imageName, data, isOfficial); + displayImageTags(result, displayName, data); elements.backToSearch.classList.add('show'); } else { showToast('获取标签失败:' + (data.error || '未知错误'), 'error'); @@ -985,96 +1086,162 @@ } } - // 显示镜像标签 - function displayImageTags(imageName, tags, isOfficial = false) { + // 显示镜像标签 + function displayImageTags(result, displayName, tags) { const fullDomain = window.location.host; + const pullImageName = displayName; - // 确保只有真正的官方镜像才处理显示名称和拉取命令 - // 使用更严格的条件:必须同时满足 isOfficial 为 true 且名称以 library/ 开头 - const isActuallyOfficial = isOfficial === true && imageName.startsWith('library/'); - const displayName = isActuallyOfficial ? imageName.substring(8) : imageName; - const pullImageName = isActuallyOfficial ? imageName.substring(8) : imageName; + // 按照原版结构构建标签页面 + const badges = []; + if (result.is_official) badges.push('官方'); + if (result.affiliation) badges.push(`By ${result.affiliation}`); - elements.tagInfo.innerHTML = ` -
- 🐳 ${displayName} + const stats = []; + if (result.star_count > 0) stats.push(`⭐ ${formatUtils.formatNumber(result.star_count)}`); + if (result.pull_count > 0) stats.push(`⬇️ ${formatUtils.formatNumber(result.pull_count)}+`); + if (result.last_updated) stats.push(`更新于 ${formatUtils.formatTimeAgo(result.last_updated)}`); + + elements.tagList.innerHTML = ` +
+
+
+ ${displayName} + ${badges.join(' ')} +
+
${result.short_description || result.description || '暂无描述'}
+
+ ${stats.join(' ')} +
+
+ docker pull ${fullDomain}/${pullImageName} + +
+
-
- 共 ${tags.length} 个标签版本 -
-
- docker pull ${fullDomain}/${pullImageName} - +
+ +
+
`; - - displayFilteredTags(tags); + + // 存储所有标签数据供搜索使用 + window.allTags = tags; + window.currentRepo = result; + window.currentDisplayName = displayName; + + // 初始显示所有标签 + renderFilteredTags(tags); // 确保显示tag列表并滚动到顶部 elements.tagList.classList.add('show'); elements.tagList.scrollIntoView({ behavior: 'smooth', block: 'start' }); } - // 显示过滤后的标签 - function displayFilteredTags(tags) { - elements.tagItems.innerHTML = ''; + // 渲染过滤后的标签(按照原版逻辑) + function renderFilteredTags(filteredTags) { + const tagsContainer = document.getElementById('tagsContainer'); + const fullDomain = window.location.host; + const pullImageName = window.currentDisplayName; - tags.forEach(tag => { - const tagItem = createTagItem(tag); - elements.tagItems.appendChild(tagItem); + let tagsHtml = filteredTags.map(tag => { + const vulnIndicators = Object.entries(tag.vulnerabilities || {}) + .map(([level, count]) => count > 0 ? `` : '') + .join(''); + + const images = tag.images || []; + const architectures = images.map(img => { + const arch = `${img.os}/${img.architecture}${img.variant ? '/' + img.variant : ''}`; + const size = formatUtils.formatSize(img.size); + return `
${arch}
`; + }).join(''); + + return ` +
+
+ ${tag.name} + ${vulnIndicators ? `
${vulnIndicators}
` : ''} +
+
+ 最后更新: ${formatUtils.formatTimeAgo(tag.last_updated)} + ${tag.last_pusher ? `由 ${tag.last_pusher} 推送` : ''} + ${tag.full_size ? `大小: ${formatUtils.formatSize(tag.full_size)}` : ''} +
+
+ docker pull ${fullDomain}/${pullImageName}:${tag.name} + +
+ ${architectures ? `
${architectures}
` : ''} +
+ `; + }).join(''); + + if (filteredTags.length === 0) { + tagsHtml = '
未找到匹配的标签
'; + } + + tagsContainer.innerHTML = tagsHtml; + } + + // 复制到剪贴板(按照原版命名) + function copyToClipboard(text) { + navigator.clipboard.writeText(text).then(() => { + showToast('已复制到剪贴板'); + }).catch(() => { + showToast('复制失败'); }); } - // 创建标签项 - function createTagItem(tag) { - const item = document.createElement('div'); - item.className = 'tag-item'; + // 标签搜索过滤(按照原版逻辑) + function filterTags(searchText) { + if (!window.allTags) return; - const architectures = tag.images?.map(img => img.architecture).join(', ') || '未知'; - const size = tag.images?.[0]?.size ? formatUtils.formatSize(tag.images[0].size) : '未知'; - const lastUpdated = tag.last_updated ? formatUtils.formatDate(tag.last_updated) : '未知'; + const searchLower = searchText.toLowerCase(); + let filteredTags; - item.innerHTML = ` -
${tag.name}
-
- 📅 更新时间: ${lastUpdated} | 📦 大小: ${size} -
-
- ${architectures.split(', ').map(arch => - `${arch}` - ).join('')} -
- `; - - return item; - } - - // 复制命令 - function copyCommand(command) { - if (navigator.clipboard) { - navigator.clipboard.writeText(command).then(() => { - showToast('命令已复制到剪贴板'); - }); + if (!searchText) { + filteredTags = window.allTags; } else { - // 降级方案 - const textarea = document.createElement('textarea'); - textarea.value = command; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); - showToast('命令已复制到剪贴板'); + // 对标签进行评分和排序 + const scoredTags = window.allTags.map(tag => { + const name = tag.name.toLowerCase(); + let score = 0; + + // 完全匹配 + if (name === searchLower) { + score += 100; + } + // 前缀匹配 + else if (name.startsWith(searchLower)) { + score += 50; + } + // 包含匹配 + else if (name.includes(searchLower)) { + score += 30; + } + // 部分匹配(按单词) + else if (searchLower.split(/\s+/).some(word => name.includes(word))) { + score += 10; + } + + return { tag, score }; + }).filter(item => item.score > 0); + + // 按分数排序 + scoredTags.sort((a, b) => b.score - a.score); + filteredTags = scoredTags.map(item => item.tag); } + + renderFilteredTags(filteredTags); } - // 标签搜索过滤 - function filterTags(searchTerm) { - const filtered = allTags.filter(tag => - tag.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); - displayFilteredTags(filtered); + // 清除标签搜索 + function clearTagSearch() { + const searchInput = document.querySelector('.tag-search-input'); + if (searchInput) { + searchInput.value = ''; + filterTags(''); + } } // 事件监听 @@ -1104,9 +1271,7 @@ searchImages(currentSearchTerm, currentPage + 1); }); - elements.tagSearchInput.addEventListener('input', (event) => { - filterTags(event.target.value); - }); + // 移除了tagSearchInput的事件监听器,现在使用内联事件 elements.backToSearch.addEventListener('click', () => { elements.tagList.classList.remove('show');