Files
hubproxy/src/public/search.html
2025-06-12 14:16:09 +08:00

1293 lines
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Docker镜像搜索">
<meta name="keywords" content="Docker,镜像搜索,docker search">
<meta name="color-scheme" content="dark light">
<title>Docker镜像搜索</title>
<link rel="icon" href="./favicon.ico">
<style>
/* 现代化设计系统 */
:root {
--background: #ffffff;
--foreground: #0f172a;
--card: #ffffff;
--card-foreground: #0f172a;
--primary: #2563eb;
--primary-foreground: #f8fafc;
--secondary: #f1f5f9;
--secondary-foreground: #0f172a;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--accent: #f1f5f9;
--accent-foreground: #0f172a;
--border: #e2e8f0;
--input: #ffffff;
--ring: #2563eb;
--radius: 0.5rem;
--success: #059669;
--warning: #d97706;
--error: #dc2626;
}
.dark {
--background: #0f172a;
--foreground: #f8fafc;
--card: #1e293b;
--card-foreground: #f8fafc;
--primary: #3b82f6;
--primary-foreground: #f8fafc;
--secondary: #1e293b;
--secondary-foreground: #f8fafc;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--accent: #1e293b;
--accent-foreground: #f8fafc;
--border: #334155;
--input: #1e293b;
--ring: #3b82f6;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0f172a;
--foreground: #f8fafc;
--card: #1e293b;
--card-foreground: #f8fafc;
--primary: #3b82f6;
--primary-foreground: #f8fafc;
--secondary: #1e293b;
--secondary-foreground: #f8fafc;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--accent: #1e293b;
--accent-foreground: #f8fafc;
--border: #334155;
--input: #1e293b;
--ring: #3b82f6;
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background-color: var(--background);
color: var(--foreground);
line-height: 1.5;
min-height: 100vh;
display: flex;
flex-direction: column;
transition: background-color 0.3s, color 0.3s;
}
/* 现代化导航栏 */
.navbar {
position: sticky;
top: 0;
z-index: 50;
width: 100%;
border-bottom: 1px solid var(--border);
background-color: var(--background);
backdrop-filter: blur(8px);
background-color: rgba(255, 255, 255, 0.95);
}
.dark .navbar {
background-color: rgba(15, 23, 42, 0.95);
}
.navbar-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
display: flex;
align-items: center;
justify-content: space-between;
height: 4rem;
}
.logo {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
color: var(--foreground);
font-weight: 600;
font-size: 1.125rem;
}
.logo-icon {
width: 2rem;
height: 2rem;
border-radius: 0.5rem;
background: linear-gradient(135deg, var(--primary), #3b82f6);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.nav-links {
display: flex;
align-items: center;
gap: 0.5rem;
}
.nav-link {
padding: 0.5rem 1rem;
border-radius: var(--radius);
text-decoration: none;
color: var(--muted-foreground);
transition: all 0.2s;
font-weight: 500;
}
.nav-link:hover,
.nav-link.active {
color: var(--foreground);
background-color: var(--muted);
}
.theme-toggle {
padding: 0.5rem;
border: none;
border-radius: var(--radius);
background-color: transparent;
color: var(--muted-foreground);
cursor: pointer;
transition: all 0.2s;
}
.theme-toggle:hover {
background-color: var(--muted);
color: var(--foreground);
}
/* 主要内容区域 */
.main {
flex: 1;
padding: 2rem 1rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.hero {
text-align: center;
margin-bottom: 3rem;
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.6s ease-out forwards;
}
.hero-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--primary), #3b82f6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: 1.125rem;
color: var(--muted-foreground);
max-width: 600px;
margin: 0 auto;
}
/* 搜索区域 */
.search-card {
background-color: var(--card);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.6s ease-out 0.2s forwards;
}
.search-container {
display: flex;
gap: 0.75rem;
align-items: stretch;
}
.search-input {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background-color: var(--input);
color: var(--foreground);
font-size: 1rem;
transition: all 0.2s;
}
.search-input:focus {
outline: none;
border-color: var(--ring);
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
}
.search-input::placeholder {
color: var(--muted-foreground);
}
.search-button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--radius);
background-color: var(--primary);
color: var(--primary-foreground);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.search-button:hover {
background-color: #1d4ed8;
transform: translateY(-1px);
}
.search-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* 加载状态 */
.loading {
display: none;
text-align: center;
padding: 3rem;
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.3s ease-out forwards;
}
.loading-spinner {
width: 3rem;
height: 3rem;
border: 3px solid var(--muted);
border-top: 3px solid var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 结果区域 */
.results-container {
opacity: 0;
transform: translateY(20px);
transition: all 0.3s;
}
.results-container.show {
opacity: 1;
transform: translateY(0);
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.results-title {
font-size: 1.25rem;
font-weight: 600;
}
.results-count {
color: var(--muted-foreground);
font-size: 0.875rem;
}
/* 结果卡片 */
.result-card {
background-color: var(--card);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
transition: all 0.2s;
cursor: pointer;
}
.result-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--primary);
}
.result-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--primary);
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.badge {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
}
.badge-official {
background-color: var(--success);
color: white;
}
.badge-organization {
background-color: var(--muted);
color: var(--muted-foreground);
}
.badge-automated {
background-color: var(--primary);
color: var(--primary-foreground);
}
.result-description {
color: var(--foreground);
margin-bottom: 1rem;
line-height: 1.6;
opacity: 0.9;
}
.result-meta {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
font-size: 0.875rem;
color: var(--muted-foreground);
}
.meta-stats {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.meta-item {
display: flex;
align-items: center;
gap: 0.25rem;
}
/* 分页 */
.pagination {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
}
.pagination-button {
padding: 0.5rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background-color: var(--card);
color: var(--foreground);
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
.pagination-button:hover:not(:disabled) {
background-color: var(--muted);
border-color: var(--primary);
}
.pagination-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 标签详情 */
.tag-list {
display: none;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s;
}
.tag-list.show {
display: block;
opacity: 1;
transform: translateY(0);
}
/* 标签头部 */
.tag-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
padding: 20px;
background-color: var(--card);
border-radius: 0.75rem;
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 {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
color: var(--primary);
font-weight: 600;
}
.tag-search-input {
width: 100%;
padding: 12px 40px 12px 15px;
border-radius: 8px;
border: 1px solid var(--border);
background-color: var(--input);
color: var(--foreground);
font-size: 1rem;
transition: all 0.3s ease;
}
.tag-search-input:focus {
outline: none;
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 {
background-color: var(--card);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.tag-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.tag-description {
color: var(--muted-foreground);
margin-bottom: 1rem;
}
.tag-pull-command {
background-color: var(--muted);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.875rem;
position: relative;
word-break: break-all;
margin-bottom: 1rem;
}
.copy-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0.25rem 0.5rem;
background-color: var(--primary);
color: var(--primary-foreground);
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.75rem;
transition: all 0.2s;
}
.copy-button:hover {
background-color: #1d4ed8;
}
/* 标签项 */
.tag-item {
background-color: var(--card);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 1rem;
transition: all 0.2s;
}
.tag-item:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.tag-name {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--primary);
}
.tag-meta {
font-size: 0.875rem;
color: var(--muted-foreground);
margin-bottom: 0.75rem;
}
.tag-architectures {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
.arch-item {
background-color: var(--muted);
color: var(--muted-foreground);
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;
}
/* 返回按钮 */
.back-to-search {
display: none;
margin-bottom: 1.5rem;
color: var(--primary);
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.back-to-search:hover {
text-decoration: underline;
}
.back-to-search.show {
display: block;
}
/* Toast 通知 */
.toast {
position: fixed;
top: 1rem;
right: 1rem;
background-color: var(--primary);
color: var(--primary-foreground);
padding: 1rem 1.5rem;
border-radius: var(--radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1001;
display: none;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
}
.toast.show {
display: block;
opacity: 1;
transform: translateX(0);
}
/* 动画 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.hero-title {
font-size: 2rem;
}
.nav-links {
display: none;
}
.search-container {
flex-direction: column;
}
.result-meta {
flex-direction: column;
align-items: flex-start;
}
.pagination {
flex-direction: column;
align-items: center;
}
}
</style>
</head>
<body>
<!-- 现代化导航栏 -->
<nav class="navbar">
<div class="navbar-container">
<a href="/" class="logo">
<div class="logo-icon">
</div>
加速服务
</a>
<div class="nav-links">
<a href="/" class="nav-link">🚀 GitHub加速</a>
<a href="/skopeo.html" class="nav-link">🐳 镜像下载</a>
<a href="/search.html" class="nav-link active">🔍 镜像搜索</a>
<a href="https://gitee.com/if-the-wind/github-hosts/raw/main/hosts" target="_blank" class="nav-link">📄 Hosts</a>
<button class="theme-toggle" id="themeToggle">
🌙
</button>
</div>
</div>
</nav>
<!-- 主要内容 -->
<main class="main">
<div class="container">
<!-- Hero 区域 -->
<div class="hero">
<h1 class="hero-title">Docker 镜像搜索</h1>
<p class="hero-subtitle">
搜索 Docker Hub 上的镜像,查看详细信息和标签版本
</p>
</div>
<!-- 搜索卡片 -->
<div class="search-card">
<div class="search-container">
<input
type="text"
class="search-input"
id="searchInput"
placeholder="输入镜像名称搜索例如nginx, mysql, ubuntu..."
>
<button class="search-button" id="searchButton">
🔍 搜索镜像
</button>
</div>
</div>
<!-- 加载状态 -->
<div class="loading" id="loading">
<div class="loading-spinner"></div>
<p>正在搜索镜像...</p>
</div>
<!-- 返回搜索按钮 -->
<div class="back-to-search" id="backToSearch">
← 返回搜索结果
</div>
<!-- 搜索结果 -->
<div class="results-container" id="searchResults">
<div class="results-header">
<h2 class="results-title">搜索结果</h2>
<span class="results-count" id="resultsCount"></span>
</div>
<div id="resultsList"></div>
<!-- 分页 -->
<div class="pagination">
<button class="pagination-button" id="prevPage" disabled>
← 上一页
</button>
<button class="pagination-button" id="nextPage" disabled>
下一页 →
</button>
</div>
</div>
<!-- 标签列表 -->
<div class="tag-list" id="tagList">
<!-- 标签页面内容将通过JavaScript动态生成 -->
</div>
</div>
</main>
<!-- Toast 通知 -->
<div id="toast" class="toast"></div>
<script>
// 主题管理
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
// 初始化主题
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
html.classList.add('dark');
themeToggle.textContent = '☀️';
}
themeToggle.addEventListener('click', () => {
html.classList.toggle('dark');
const isDark = html.classList.contains('dark');
themeToggle.textContent = isDark ? '☀️' : '🌙';
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
// 全局变量
let currentPage = 1;
let isLoading = false;
let currentSearchTerm = '';
let searchResults = [];
let allTags = [];
// DOM 元素
const elements = {
searchInput: document.getElementById('searchInput'),
searchButton: document.getElementById('searchButton'),
loading: document.getElementById('loading'),
searchResults: document.getElementById('searchResults'),
resultsList: document.getElementById('resultsList'),
resultsCount: document.getElementById('resultsCount'),
prevPage: document.getElementById('prevPage'),
nextPage: document.getElementById('nextPage'),
tagList: document.getElementById('tagList'),
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+';
return num.toString();
},
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 '未知时间';
}
},
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]}`;
}
};
// Toast 通知
function showToast(message, type = 'info') {
const toast = elements.toast;
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// 搜索功能
async function searchImages(term, page = 1) {
if (isLoading) return;
isLoading = true;
currentSearchTerm = term;
currentPage = page;
elements.loading.style.display = 'block';
elements.searchResults.classList.remove('show');
elements.tagList.classList.remove('show');
elements.backToSearch.classList.remove('show');
try {
const response = await fetch(`/search?q=${encodeURIComponent(term)}&page=${page}`);
const data = await response.json();
if (data && data.results) {
searchResults = data.results;
displaySearchResults(data.results, data.count, page);
} else {
showToast('搜索失败:' + (data.error || '未知错误'), 'error');
}
} catch (error) {
console.error('搜索错误:', error);
showToast('搜索失败,请检查网络连接', 'error');
} finally {
isLoading = false;
elements.loading.style.display = 'none';
}
}
// 显示搜索结果
function displaySearchResults(results, total, page) {
elements.resultsList.innerHTML = '';
elements.resultsCount.textContent = `共找到 ${total} 个镜像`;
results.forEach(result => {
const resultCard = createResultCard(result);
elements.resultsList.appendChild(resultCard);
});
// 更新分页按钮
elements.prevPage.disabled = page <= 1;
elements.nextPage.disabled = results.length < 25;
elements.searchResults.classList.add('show');
}
// 创建结果卡片
function createResultCard(result) {
const card = document.createElement('div');
card.className = 'result-card';
// 按照原版逻辑构建显示名称
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(result, displayName);
const badges = [];
if (result.is_official) badges.push('<span class="badge badge-official">官方</span>');
if (result.is_automated) badges.push('<span class="badge badge-automated">自动构建</span>');
card.innerHTML = `
<div class="result-title">
🐳 ${displayName}
${badges.join('')}
</div>
<div class="result-description">
${result.short_description || result.description || '暂无描述'}
</div>
<div class="result-meta">
<div class="meta-stats">
<div class="meta-item">
<span>📈</span>
${formatUtils.formatNumber(result.star_count || 0)} Stars
</div>
<div class="meta-item">
<span>📥</span>
${formatUtils.formatNumber(result.pull_count || 0)} Pulls
</div>
</div>
</div>
`;
return card;
}
// 查看镜像标签
async function viewImageTags(result, displayName) {
if (isLoading) return;
isLoading = true;
elements.loading.style.display = 'block';
elements.searchResults.classList.remove('show');
try {
// 获取命名空间和仓库名
const namespace = result.namespace || (result.is_official ? 'library' : '');
const name = result.name || result.repo_name || '';
const repoName = name.replace('library/', '');
if (!namespace || !repoName) {
showToast('无效的仓库信息', 'error');
return;
}
const response = await fetch(`/tags/${namespace}/${repoName}`);
const data = await response.json();
if (Array.isArray(data)) {
allTags = data;
displayImageTags(result, displayName, data);
elements.backToSearch.classList.add('show');
} else {
showToast('获取标签失败:' + (data.error || '未知错误'), 'error');
}
} catch (error) {
console.error('获取标签错误:', error);
showToast('获取标签失败,请检查网络连接', 'error');
} finally {
isLoading = false;
elements.loading.style.display = 'none';
}
}
// 显示镜像标签
function displayImageTags(result, displayName, tags) {
const fullDomain = window.location.host;
const pullImageName = displayName;
// 按照原版结构构建标签页面
const badges = [];
if (result.is_official) badges.push('<span class="badge badge-official">官方</span>');
if (result.affiliation) badges.push(`<span class="badge badge-organization">By ${result.affiliation}</span>`);
const stats = [];
if (result.star_count > 0) stats.push(`<span class="meta-item">⭐ ${formatUtils.formatNumber(result.star_count)}</span>`);
if (result.pull_count > 0) stats.push(`<span class="meta-item">⬇️ ${formatUtils.formatNumber(result.pull_count)}+</span>`);
if (result.last_updated) stats.push(`<span class="meta-item">更新于 ${formatUtils.formatTimeAgo(result.last_updated)}</span>`);
elements.tagList.innerHTML = `
<div class="tag-header">
<div class="tag-info">
<div class="tag-title">
${displayName}
${badges.join(' ')}
</div>
<div class="tag-description">${result.short_description || result.description || '暂无描述'}</div>
<div class="tag-meta">
${stats.join(' ')}
</div>
<div class="tag-pull-command">
docker pull ${fullDomain}/${pullImageName}
<button class="copy-button" onclick="copyToClipboard('docker pull ${fullDomain}/${pullImageName}')">复制</button>
</div>
</div>
</div>
<div class="tag-search-container">
<input type="text" class="tag-search-input" placeholder="输入关键词搜索标签,支持模糊匹配..." oninput="filterTags(this.value)">
<button class="tag-search-clear" onclick="clearTagSearch()">×</button>
</div>
<div id="tagsContainer"></div>
`;
// 存储所有标签数据供搜索使用
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 renderFilteredTags(filteredTags) {
const tagsContainer = document.getElementById('tagsContainer');
const fullDomain = window.location.host;
const pullImageName = window.currentDisplayName;
let tagsHtml = filteredTags.map(tag => {
const vulnIndicators = Object.entries(tag.vulnerabilities || {})
.map(([level, count]) => count > 0 ? `<span class="vulnerability-dot vulnerability-${level.toLowerCase()}" title="${level}: ${count}"></span>` : '')
.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 `<div class="arch-item" title="大小: ${size}">${arch}</div>`;
}).join('');
return `
<div class="tag-item">
<div class="tag-name">
${tag.name}
${vulnIndicators ? `<div class="vulnerability-indicator">${vulnIndicators}</div>` : ''}
</div>
<div class="tag-meta">
<span>最后更新: ${formatUtils.formatTimeAgo(tag.last_updated)}</span>
${tag.last_pusher ? `<span>由 ${tag.last_pusher} 推送</span>` : ''}
${tag.full_size ? `<span>大小: ${formatUtils.formatSize(tag.full_size)}</span>` : ''}
</div>
<div class="tag-pull-command">
docker pull ${fullDomain}/${pullImageName}:${tag.name}
<button class="copy-button" onclick="copyToClipboard('docker pull ${fullDomain}/${pullImageName}:${tag.name}')">复制</button>
</div>
${architectures ? `<div class="tag-architectures">${architectures}</div>` : ''}
</div>
`;
}).join('');
if (filteredTags.length === 0) {
tagsHtml = '<div class="text-center" style="padding: 20px;">未找到匹配的标签</div>';
}
tagsContainer.innerHTML = tagsHtml;
}
// 复制到剪贴板(按照原版命名)
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showToast('已复制到剪贴板');
}).catch(() => {
showToast('复制失败');
});
}
// 标签搜索过滤(按照原版逻辑)
function filterTags(searchText) {
if (!window.allTags) return;
const searchLower = searchText.toLowerCase();
let filteredTags;
if (!searchText) {
filteredTags = window.allTags;
} else {
// 对标签进行评分和排序
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 clearTagSearch() {
const searchInput = document.querySelector('.tag-search-input');
if (searchInput) {
searchInput.value = '';
filterTags('');
}
}
// 事件监听
elements.searchButton.addEventListener('click', () => {
const term = elements.searchInput.value.trim();
if (term) {
searchImages(term);
}
});
elements.searchInput.addEventListener('keyup', (event) => {
if (event.key === 'Enter') {
const term = elements.searchInput.value.trim();
if (term) {
searchImages(term);
}
}
});
elements.prevPage.addEventListener('click', () => {
if (currentPage > 1) {
searchImages(currentSearchTerm, currentPage - 1);
}
});
elements.nextPage.addEventListener('click', () => {
searchImages(currentSearchTerm, currentPage + 1);
});
// 移除了tagSearchInput的事件监听器现在使用内联事件
elements.backToSearch.addEventListener('click', () => {
elements.tagList.classList.remove('show');
elements.backToSearch.classList.remove('show');
elements.searchResults.classList.add('show');
});
// 页面初始化
document.addEventListener('DOMContentLoaded', () => {
// 隐藏初始状态的元素
elements.searchResults.classList.remove('show');
elements.tagList.classList.remove('show');
elements.backToSearch.classList.remove('show');
});
</script>
</body>
</html>