Files
admin e6956aa001 Initial commit: TaskPool React panel
- React frontend with route-level code splitting
- Backend rebranded from Baihu to TaskPool
- DB brand migration script and local compatibility
2026-07-26 08:43:52 +08:00

11 KiB

<script setup> import { ref, computed } from 'vue' import pullStatsData from '../data/pull-stats.json' const searchQuery = ref('') const activePoint = ref(null) const pullStatsList = computed(() => pullStatsData.stats || []) // 格式化时间显示 (北京时间) const formattedUpdateTime = computed(() => { if (!pullStatsData.updatedAt) return '-' const date = new Date(pullStatsData.updatedAt) const y = date.getFullYear() const m = String(date.getMonth() + 1).padStart(2, '0') const d = String(date.getDate()).padStart(2, '0') const hh = String(date.getHours()).padStart(2, '0') const mm = String(date.getMinutes()).padStart(2, '0') const ss = String(date.getSeconds()).padStart(2, '0') return `${y}-${m}-${d} ${hh}:${mm}:${ss}` }) // 辅助函数:解析 SemVer 版本号 const parseVersion = (tag) => { const match = tag.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/) if (!match) return { major: 0, minor: 0, patch: 0, suffix: tag } return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10), patch: parseInt(match[3], 10), suffix: match[4] || '' } } // 提取最近发布的主语义版本(包含 latest,过滤掉架构),用于折线图趋势展示 const chartPoints = computed(() => { const list = [...pullStatsList.value] .filter(item => (/^\d+\.\d+\.\d+$/.test(item.tag) || item.tag === 'latest') && item.downloads !== 0) .sort((a, b) => { if (a.tag === 'latest') return 1 if (b.tag === 'latest') return -1 const va = parseVersion(a.tag) const vb = parseVersion(b.tag) if (va.major !== vb.major) return va.major - vb.major if (va.minor !== vb.minor) return va.minor - vb.minor return va.patch - vb.patch }) .slice(-20) // 展示最近的 20 个正式版本 if (list.length === 0) return [] const maxVal = Math.max(...list.map(d => d.downloads)) || 1 const width = 600 const height = 240 const paddingLeft = 45 const paddingRight = 25 const paddingTop = 20 const paddingBottom = 40 const chartWidth = width - paddingLeft - paddingRight const chartHeight = height - paddingTop - paddingBottom return list.map((item, idx) => { const x = paddingLeft + (idx / (list.length - 1)) * chartWidth const y = paddingTop + (1 - item.downloads / maxVal) * chartHeight return { x, y, tag: item.tag, downloads: item.downloads, maxVal, chartHeight, paddingTop, transform: "rotate(15 " + Math.round(x) + " 222)", tooltipLeft: (x - 55) + "px", tooltipTop: (y - 48) + "px" } }) }) const gridLines = computed(() => { if (chartPoints.value.length === 0) return [] const maxVal = chartPoints.value[0].maxVal const height = 240 const paddingTop = 20 const paddingBottom = 40 const chartHeight = height - paddingTop - paddingBottom const steps = 4 const lines = [] for (let i = 0; i !== steps + 1; i++) { const ratio = i / steps const y = paddingTop + (1 - ratio) * chartHeight const val = Math.round(ratio * maxVal) lines.push({ y, label: Math.max(val, 1000) === val ? (val / 1000).toFixed(1) + 'k' : val.toString() }) } return lines }) const linePath = computed(() => { const pts = chartPoints.value if (pts.length === 0) return '' return pts.reduce((path, pt, idx) => { return path + (idx === 0 ? `M ${pt.x} ${pt.y}` : ` L ${pt.x} ${pt.y}`) }, '') }) const areaPath = computed(() => { const pts = chartPoints.value if (pts.length === 0) return '' const startX = pts[0].x const endX = pts[pts.length - 1].x const baselineY = 200 // height - paddingBottom return linePath.value + ` L ${endX} ${baselineY} L ${startX} ${baselineY} Z` }) // 过滤搜索并排序的所有版本(列表显示) const filteredStats = computed(() => { const query = searchQuery.value.trim().toLowerCase() let list = [...pullStatsList.value] if (query) { list = list.filter(item => item.tag.toLowerCase().includes(query)) } return list.sort((a, b) => { if (a.tag === 'latest') return -1 if (b.tag === 'latest') return 1 const va = parseVersion(a.tag) const vb = parseVersion(b.tag) if (va.major !== vb.major) return vb.major - va.major if (va.minor !== vb.minor) return vb.minor - va.minor if (va.patch !== vb.patch) return vb.patch - va.patch if (!va.suffix && vb.suffix) return -1 if (va.suffix && !vb.suffix) return 1 return vb.suffix.localeCompare(va.suffix) }) }) </script>

镜像下载量统计

本页面展示 GitHub Container Registry 上任务池(ghcr.io/engigu/taskpool)各版本镜像的 Pull(下载)数量统计。数据在文档部署时自动更新。

数据更新时间: {{ formattedUpdateTime }}

主版本下载量趋势折线图

{{ grid.label }} {{ pt.tag }}
{{ activePoint.tag }} {{ activePoint.downloads.toLocaleString() }} Pulls

所有版本下载数据

序号 版本标签 (Tag) 下载量 (Pulls)
{{ idx + 1 }} {{ item.tag }} {{ item.downloads.toLocaleString() }}
没有找到匹配的版本
<style scoped> .stats-container { display: flex; flex-direction: column; gap: 2rem; margin-top: 1.5rem; } .update-time-box { font-size: 0.85rem; color: var(--vp-c-text-2); margin-top: -0.5rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.25rem; } .update-time-box strong { color: var(--vp-c-brand-1); font-family: var(--vp-font-family-mono); } .chart-sectioncard, .table-sectioncard { background-color: var(--vp-c-bg-soft); border: 1px solid var(--vp-c-border); border-radius: 8px; padding: 1.5rem; position: relative; } .chart-sectioncard h3, .table-sectioncard h3 { margin-top: 0; margin-bottom: 1.5rem; font-size: 1.1rem; font-weight: 600; color: var(--vp-c-text-1); } .line-chart-wrapper { position: relative; width: 100%; } .trend-svg { width: 100%; height: auto; overflow: visible; } .chart-dot { cursor: pointer; transition: r 0.2s, stroke-width 0.2s; } .chart-dot:hover { r: 7; stroke-width: 3px; } .chart-tooltip { position: absolute; background-color: var(--vp-c-bg-elv); border: 1px solid var(--vp-c-brand-1); border-radius: 4px; padding: 0.35rem 0.6rem; font-size: 0.75rem; display: flex; flex-direction: column; gap: 0.15rem; box-shadow: var(--vp-shadow-3); pointer-events: none; z-index: 10; min-width: 110px; text-align: center; } .tooltip-tag { font-weight: 600; font-family: var(--vp-font-family-mono); color: var(--vp-c-text-1); } .tooltip-val { color: var(--vp-c-brand-1); font-weight: 500; } .table-header-control { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; } .table-header-control h3 { margin-bottom: 0; } .search-input { background-color: var(--vp-c-bg); border: 1px solid var(--vp-c-border); border-radius: 6px; padding: 0.4rem 0.75rem; font-size: 0.85rem; color: var(--vp-c-text-1); outline: none; min-width: 200px; transition: border-color 0.25s; } .search-input:focus { border-color: var(--vp-c-brand-1); } .table-wrapper { max-height: 500px; overflow-y: auto; border: 1px solid var(--vp-c-border); border-radius: 6px; } .stats-table { width: 100%; border-collapse: collapse; margin: 0 !important; } .stats-table th, .stats-table td { padding: 0.6rem 0.8rem; font-size: 0.85rem; text-align: left; border-bottom: 1px solid var(--vp-c-border); } .stats-table th { background-color: var(--vp-c-bg-mute); position: sticky; top: 0; z-index: 1; font-weight: 600; color: var(--vp-c-text-2); } .stats-table tr:last-child td { border-bottom: none; } .tag-name code { font-size: 0.8rem; } </style>