docs: add ghcr package downloads trend chart page and build-time scraper
This commit is contained in:
@@ -4,6 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main, docs]
|
branches: [main, docs]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 1 * * *'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -47,6 +49,9 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
working-directory: docs
|
working-directory: docs
|
||||||
|
|
||||||
|
- name: Fetch Package Download Stats
|
||||||
|
run: node docs/fetch-stats.js
|
||||||
|
|
||||||
- name: Build with VitePress
|
- name: Build with VitePress
|
||||||
run: npm run docs:build
|
run: npm run docs:build
|
||||||
working-directory: docs
|
working-directory: docs
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
text: '其他',
|
text: '其他',
|
||||||
items: [
|
items: [
|
||||||
|
{ text: '镜像下载量', link: '/guide/package-stats' },
|
||||||
{ text: '更新日志', link: '/guide/changelog' },
|
{ text: '更新日志', link: '/guide/changelog' },
|
||||||
{ text: '免责声明', link: '/guide/disclaimer' }
|
{ text: '免责声明', link: '/guide/disclaimer' }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
const { execSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
async function scrapeAll() {
|
||||||
|
const allResults = [];
|
||||||
|
const seenTags = new Set();
|
||||||
|
let page = 1;
|
||||||
|
let reachedEnd = false;
|
||||||
|
while (!reachedEnd) {
|
||||||
|
console.log(`Fetching page ${page}...`);
|
||||||
|
const url = `https://github.com/engigu/baihu-panel/pkgs/container/baihu/versions?page=${page}`;
|
||||||
|
let html;
|
||||||
|
let success = false;
|
||||||
|
for (let retry = 1; retry <= 3; retry++) {
|
||||||
|
try {
|
||||||
|
html = execSync(`curl -sL "${url}"`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 10 });
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to fetch page ${page} (attempt ${retry}/3):`, e.message);
|
||||||
|
if (retry < 3) {
|
||||||
|
console.log(`Waiting 5s before retrying...`);
|
||||||
|
await new Promise(r => setTimeout(r, 5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
console.error(`Giving up on page ${page}.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boxRows = html.split('class="Box-row"');
|
||||||
|
if (boxRows.length <= 1) {
|
||||||
|
console.log(`No versions found on page ${page}. Stopping.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedCount = 0;
|
||||||
|
for (let i = 1; i < boxRows.length; i++) {
|
||||||
|
const row = boxRows[i];
|
||||||
|
const tagMatch = row.match(/\?tag=([^"]+)"[^>]*>([^<]+)<\/a>/);
|
||||||
|
if (!tagMatch) continue;
|
||||||
|
const tag = tagMatch[1];
|
||||||
|
|
||||||
|
if (seenTags.has(tag)) {
|
||||||
|
console.log(`Duplicate tag "${tag}" detected. Reached end of registry pages.`);
|
||||||
|
reachedEnd = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
seenTags.add(tag);
|
||||||
|
|
||||||
|
// 只保留形如 1.1.15、1.1.15-minimal、1.1.15-debian13 以及 latest 等主版本及其不同架构/后缀版本
|
||||||
|
if (!/^(latest|\d+\.\d+\.\d+)/.test(tag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadsMatch = row.match(/([\d,]+)\s*<span class="sr-only">Version downloads<\/span>/);
|
||||||
|
const downloads = downloadsMatch ? parseInt(downloadsMatch[1].replace(/,/g, ''), 10) : 0;
|
||||||
|
|
||||||
|
allResults.push({ tag, downloads });
|
||||||
|
parsedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reachedEnd) break;
|
||||||
|
|
||||||
|
console.log(`Parsed ${parsedCount} versions from page ${page}.`);
|
||||||
|
|
||||||
|
// 每页保存一次,防止后面的页面超时或出错导致前面的数据丢失
|
||||||
|
const destDir = path.join(__dirname, './data');
|
||||||
|
if (!fs.existsSync(destDir)) {
|
||||||
|
fs.mkdirSync(destDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const destPath = path.join(destDir, 'pull-stats.json');
|
||||||
|
const outputData = {
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
stats: allResults
|
||||||
|
};
|
||||||
|
fs.writeFileSync(destPath, JSON.stringify(outputData, null, 2));
|
||||||
|
console.log(`Saved ${allResults.length} versions (up to page ${page}) to ${destPath}`);
|
||||||
|
|
||||||
|
page++;
|
||||||
|
// Sleep 1s to avoid hitting rate limits
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrapeAll();
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
<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] || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取最近发布的主语义版本(例如1.1.15,过滤掉架构与latest),用于折线图趋势展示
|
||||||
|
const chartPoints = computed(() => {
|
||||||
|
const list = [...pullStatsList.value]
|
||||||
|
.filter(item => /^\d+\.\d+\.\d+$/.test(item.tag) && item.downloads !== 0)
|
||||||
|
.sort((a, b) => {
|
||||||
|
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(-12) // 展示最近的 12 个正式版本
|
||||||
|
|
||||||
|
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/baihu`)各版本镜像的 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>
|
||||||
Generated
+1726
-2651
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user