Skip to content

Commit c3b7140

Browse files
committed
feat: 添加 UmamiStats 组件并在导航栏中显示网站统计信息
1 parent 4db0fc1 commit c3b7140

3 files changed

Lines changed: 180 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<script setup lang="ts">
2+
import { computed, onMounted, ref } from "vue";
3+
import { useData } from "vitepress";
4+
5+
const { lang } = useData();
6+
7+
// Umami 配置
8+
const UMAMI_BASE_URL = "https://umami.micyou.top";
9+
const UMAMI_SHARE_ID = "ca3lqhEXL5TaJrwv";
10+
11+
// 翻译
12+
const translations: Record<string, { views: string; visits: string; loading: string }> = {
13+
"zh-CN": { views: "浏览量", visits: "访问次数", loading: "加载中..." },
14+
en: { views: "Views", visits: "Visits", loading: "Loading..." },
15+
"zh-TW": { views: "瀏覽量", visits: "訪問次數", loading: "載入中..." },
16+
};
17+
18+
const t = computed(() => translations[lang.value] || translations["zh-CN"]);
19+
20+
// 统计数据类型
21+
interface Stats {
22+
pageviews: number;
23+
visitors: number;
24+
visits: number;
25+
}
26+
27+
// 状态
28+
const stats = ref<Stats | null>(null);
29+
const loading = ref(true);
30+
31+
// 获取分享信息(实时)
32+
async function fetchShareInfo(baseUrl: string, shareId: string): Promise<{ websiteId: string; token: string }> {
33+
const response = await fetch(`${baseUrl}/api/share/${encodeURIComponent(shareId)}`);
34+
if (!response.ok) {
35+
throw new Error(`Failed to fetch share info: ${response.status}`);
36+
}
37+
38+
const data = await response.json();
39+
if (!data?.token || !data?.websiteId) {
40+
throw new Error("Invalid share info response");
41+
}
42+
43+
return data;
44+
}
45+
46+
// 获取统计数据(实时)
47+
async function fetchStats(): Promise<void> {
48+
try {
49+
const shareInfo = await fetchShareInfo(UMAMI_BASE_URL, UMAMI_SHARE_ID);
50+
const endAt = Date.now();
51+
const startAt = 0;
52+
53+
const response = await fetch(
54+
`${UMAMI_BASE_URL}/api/websites/${encodeURIComponent(shareInfo.websiteId)}/stats?startAt=${startAt}&endAt=${endAt}`,
55+
{
56+
headers: {
57+
"x-umami-share-token": shareInfo.token,
58+
},
59+
}
60+
);
61+
62+
if (!response.ok) {
63+
throw new Error(`Failed to fetch stats: ${response.status}`);
64+
}
65+
66+
const data = await response.json();
67+
stats.value = data;
68+
} catch (err) {
69+
console.error("Failed to load Umami stats:", err);
70+
} finally {
71+
loading.value = false;
72+
}
73+
}
74+
75+
// 格式化数字
76+
function formatNumber(num: number): string {
77+
if (num >= 10000) {
78+
return `${(num / 10000).toFixed(1)}w`;
79+
}
80+
if (num >= 1000) {
81+
return `${(num / 1000).toFixed(1)}k`;
82+
}
83+
return num.toString();
84+
}
85+
86+
onMounted(() => {
87+
fetchStats();
88+
});
89+
</script>
90+
91+
<template>
92+
<div class="umami-notice" aria-label="Website Statistics">
93+
<span class="notice-icon">📊</span>
94+
<span v-if="loading" class="notice-loading">{{ t.loading }}</span>
95+
<span v-else-if="stats" class="notice-content">
96+
<span class="stat">
97+
<strong>{{ formatNumber(stats.pageviews || 0) }}</strong>
98+
<span class="label">{{ t.views }}</span>
99+
</span>
100+
<span class="divider">·</span>
101+
<span class="stat">
102+
<strong>{{ formatNumber(stats.visits || 0) }}</strong>
103+
<span class="label">{{ t.visits }}</span>
104+
</span>
105+
</span>
106+
</div>
107+
</template>
108+
109+
<style scoped>
110+
.umami-notice {
111+
display: inline-flex;
112+
align-items: center;
113+
gap: 6px;
114+
padding: 4px 12px;
115+
font-size: 0.75rem;
116+
color: var(--vp-c-text-2);
117+
}
118+
119+
.notice-icon {
120+
font-size: 0.875rem;
121+
}
122+
123+
.notice-loading {
124+
color: var(--vp-c-text-3);
125+
}
126+
127+
.notice-content {
128+
display: flex;
129+
align-items: center;
130+
gap: 6px;
131+
}
132+
133+
.stat {
134+
display: flex;
135+
align-items: baseline;
136+
gap: 3px;
137+
}
138+
139+
.stat strong {
140+
color: var(--vp-c-brand-1);
141+
font-weight: 600;
142+
}
143+
144+
.stat .label {
145+
color: var(--vp-c-text-3);
146+
font-size: 0.7rem;
147+
}
148+
149+
.divider {
150+
color: var(--vp-c-divider);
151+
margin: 0 2px;
152+
}
153+
154+
/* 响应式 */
155+
@media (max-width: 960px) {
156+
.umami-notice {
157+
display: none;
158+
}
159+
}
160+
</style>

.vitepress/theme/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { getFooterData, type Lang } from "../data/i18n";
1717
import Contributors from "./components/ContributorsCards/Contributors.vue";
1818
import DownloadSection from "./components/DownloadSection/DownloadSection.vue";
19+
import UmamiStats from "./components/UmamiStats.vue";
1920
import WebVitals from "./components/WebVitals.vue";
2021
import "./style.css";
2122

@@ -39,6 +40,12 @@ export default {
3940
skipText[lang.value] || skipText["zh-CN"],
4041
);
4142
},
43+
"nav-bar-content-after": () => {
44+
const { frontmatter } = useData();
45+
// 只在首页显示统计
46+
const isHome = frontmatter.value.layout === "home";
47+
return isHome ? h("div", { class: "nav-stats-center" }, h(UmamiStats)) : null;
48+
},
4249
"layout-bottom": () => {
4350
// 从 VitePress 获取当前语言
4451
const { lang } = useData();
@@ -57,6 +64,7 @@ export default {
5764
app.component("Copy", CopyText);
5865
app.component("Contributors", Contributors);
5966
app.component("DownloadSection", DownloadSection);
67+
app.component("UmamiStats", UmamiStats);
6068
// 注册 Umami Analytics 插件 - 延迟加载优化 INP
6169
if (typeof window !== "undefined") {
6270
// 使用 requestIdleCallback 延迟加载分析脚本

.vitepress/theme/style.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ iframe {
7676
top: 16px;
7777
}
7878

79+
/* 导航栏中间统计栏 */
80+
.VPNavBar {
81+
position: relative;
82+
}
83+
84+
.nav-stats-center {
85+
position: absolute;
86+
left: 50%;
87+
top: 50%;
88+
transform: translate(-50%, -50%);
89+
}
90+
7991
:focus-visible {
8092
outline: 2px solid var(--vp-c-brand-1);
8193
outline-offset: 2px;

0 commit comments

Comments
 (0)