Files
TaskPool/docs/guide/package-stats.md
T
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

389 lines
11 KiB
Markdown

<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(下载)数量统计。数据在文档部署时自动更新。
<div class="update-time-box">
<span>数据更新时间:</span>
<strong>{{ formattedUpdateTime }}</strong>
</div>
<div class="stats-container">
<div class="chart-sectioncard">
<h3>主版本下载量趋势折线图</h3>
<div class="line-chart-wrapper">
<svg viewBox="0 0 600 240" class="trend-svg">
<defs>
<linearGradient id="chart-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--vp-c-brand-1)" stop-opacity="0.25"></stop>
<stop offset="100%" stop-color="var(--vp-c-brand-1)" stop-opacity="0.0"></stop>
</linearGradient>
</defs>
<g stroke="var(--vp-c-divider)" stroke-dasharray="3,3" stroke-width="1">
<line v-for="grid in gridLines" :key="grid.y" x1="45" :y1="grid.y" x2="575" :y2="grid.y"></line>
</g>
<g fill="var(--vp-c-text-3)" font-size="11" font-family="var(--vp-font-family-base)" text-anchor="end">
<text v-for="grid in gridLines" :key="grid.y" x="38" :y="grid.y + 4">{{ grid.label }}</text>
</g>
<path :d="areaPath" fill="url(#chart-grad)"></path>
<path :d="linePath" fill="none" stroke="var(--vp-c-brand-1)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"></path>
<g>
<circle
v-for="(pt, idx) in chartPoints"
:key="idx"
:cx="pt.x"
:cy="pt.y"
r="5"
fill="var(--vp-c-bg)"
stroke="var(--vp-c-brand-1)"
stroke-width="2"
class="chart-dot"
@mouseenter="activePoint = pt"
@mouseleave="activePoint = null"
></circle>
</g>
<g fill="var(--vp-c-text-2)" font-size="11" font-family="var(--vp-font-family-base)" text-anchor="middle">
<text
v-for="(pt, idx) in chartPoints"
:key="idx"
:x="pt.x"
y="222"
:transform="pt.transform"
>
{{ pt.tag }}
</text>
</g>
</svg>
<div v-if="activePoint" class="chart-tooltip" :style="{ left: activePoint.tooltipLeft, top: activePoint.tooltipTop }">
<span class="tooltip-tag">{{ activePoint.tag }}</span>
<span class="tooltip-val">{{ activePoint.downloads.toLocaleString() }} Pulls</span>
</div>
</div>
</div>
<div class="table-sectioncard">
<div class="table-header-control">
<h3>所有版本下载数据</h3>
<input type="text" v-model="searchQuery" placeholder="搜索版本标签..." class="search-input" />
</div>
<div class="table-wrapper">
<table class="stats-table">
<thead>
<tr>
<th>序号</th>
<th>版本标签 (Tag)</th>
<th style="text-align: right;">下载量 (Pulls)</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, idx) in filteredStats" :key="item.tag">
<td>{{ idx + 1 }}</td>
<td class="tag-name"><code>{{ item.tag }}</code></td>
<td style="text-align: right; font-weight: 500;">{{ item.downloads.toLocaleString() }}</td>
</tr>
<tr v-if="filteredStats.length === 0">
<td colspan="3" style="text-align: center; color: var(--vp-c-text-3); padding: 2rem 0;">没有找到匹配的版本</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<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>