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 >
0 commit comments