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 = ` -