feat: 添加 Umami 分析支持,更新配置文档

This commit is contained in:
2025-12-07 14:27:42 +08:00
parent d7b419f401
commit 361999a8b1
9 changed files with 118 additions and 70 deletions

View File

@@ -1,5 +1,9 @@
# Github Token
VITE_GITHUB_TOKEN=your-github-token VITE_GITHUB_TOKEN=your-github-token
# UMAMI API KEY
VITE_UMAMI_API_KEY=your-umami-api-key
# SMTP 服务器地址 # SMTP 服务器地址
SMTP_HOST=smtp.example.com SMTP_HOST=smtp.example.com

View File

@@ -28,6 +28,10 @@ const siteConfig: SiteConfig = {
title: "I'm a software developer.", // 你的简介,可为空 title: "I'm a software developer.", // 你的简介,可为空
avatar: "avatar.webp", // 你的头像可为public目录下的文件或外部链接 avatar: "avatar.webp", // 你的头像可为public目录下的文件或外部链接
bio: "Hello World", // 你的喜欢的一句话,可为空 bio: "Hello World", // 你的喜欢的一句话,可为空
birthday: "xxxx-xx-xx", // 你的生日,可为空
gender: "", // 你的性别,可为空
pronouns: "", // 你希望别人如何称呼你,可为空
location: "", // 你的居住地,可为空
}, },
// 社交链接,预定义的社交链接可在 `src/components/SocialLink.vue` 中查阅 // 社交链接,预定义的社交链接可在 `src/components/SocialLink.vue` 中查阅
@@ -52,8 +56,16 @@ const siteConfig: SiteConfig = {
], ],
siteMeta: { siteMeta: {
title: "RhenCloud", // 网站标题 title: "Example Title", // 网站标题
icon: "favicon.ico", // 网站图标可为public目录下的文件或外部链接 icon: "favicon.ico", // 网站图标可为public目录下的文件或外部链接
startDate:"xxxx-xx-xx", // 网站创建日期
},
umami: {
enable: true, // 是否启用 Umami 分析
url: "https://cloud.umami.is/script.js", // Umami 分析脚本 URL一般无需修改
websiteId: "YOUR_WEBSITE_ID", // Umami 网站 ID
apiBase: "https://api.umami.is", // Umami API 地址,一般无需修改
}, },
// 技能图标展示详见https://github.com/tandpfun/skill-icons#icons-list // 技能图标展示详见https://github.com/tandpfun/skill-icons#icons-list
@@ -100,9 +112,11 @@ const siteConfig: SiteConfig = {
footer: { footer: {
beian: "备案号", // 备案号,留空则不显示 beian: "备案号", // 备案号,留空则不显示
beianLink: "https://beian.miit.gov.cn/", // 备案号链接,一般无需修改 beianLink: "https://beian.miit.gov.cn/", // 备案号链接,一般无需修改
showHitokoto: true, // 是否显示一言
hitokotoType: "a&b&c&d&j", // 一言类型,详见 https://developer.hitokoto.cn/sentence/#%E5%8F%A5%E5%AD%90%E7%B1%BB%E5%9E%8B-%E5%8F%82%E6%95%B0
customHtml: '', // 自定义 HTML 代码,如统计代码等 customHtml: '', // 自定义 HTML 代码,如统计代码等
hitokoto: {
enable: true, // 是否启用一言
type: "a&b&c&d&j", // 一言类型,详见 https://developer.hitokoto.cn/sentence/#%E5%8F%A5%E5%AD%90%E7%B1%BB%E5%9E%8B-%E5%8F%82%E6%95%B0
},
}, },
}; };
``` ```
@@ -112,11 +126,12 @@ const siteConfig: SiteConfig = {
- **404 页面**:修改 `public/404.html` 来自定义 404 错误页面的样式与内容。 - **404 页面**:修改 `public/404.html` 来自定义 404 错误页面的样式与内容。
- **友链展示逻辑**`FriendsSection.vue` 默认使用随机顺序渲染 `siteConfig.friends`,如需固定顺序请修改该组件。 - **友链展示逻辑**`FriendsSection.vue` 默认使用随机顺序渲染 `siteConfig.friends`,如需固定顺序请修改该组件。
## 环境变量(邮件发送) ## 环境变量
在 Vercel 控制台或本地 `.env` 配置: 在 Vercel 控制台或本地 `.env` 配置:
- `VITE_GITHUB_TOKEN`: 具有仓库读取权限的 GitHub Token用于绕过 GitHub API 速率限制。 - `VITE_GITHUB_TOKEN`: 具有仓库读取权限的 GitHub Token用于绕过 GitHub API 速率限制。(可选)
- `UMAMI_API_KEY`: Umami 分析的 API Key。
- `SMTP_HOST`: 邮件服务器主机名 - `SMTP_HOST`: 邮件服务器主机名
- `SMTP_PORT`: 端口(如 465 或 587 - `SMTP_PORT`: 端口(如 465 或 587
- `SMTP_USER`: 发件人邮箱账号 - `SMTP_USER`: 发件人邮箱账号
@@ -140,7 +155,7 @@ pnpm dev
pnpm build pnpm build
``` ```
产物输出到 `dist/` 产物输出到
## 部署到 Vercel ## 部署到 Vercel

View File

@@ -8,6 +8,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@jaseeey/vue-umami-plugin": "^1.4.0",
"@vercel/node": "^5.5.15", "@vercel/node": "^5.5.15",
"express": "^5.2.1", "express": "^5.2.1",
"he": "^1.2.0", "he": "^1.2.0",

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

View File

@@ -9,6 +9,7 @@
</template> </template>
<script setup> <script setup>
import { onMounted } from "vue";
import PageSwitcher from "./components/PageSwitcher.vue"; import PageSwitcher from "./components/PageSwitcher.vue";
import FooterSection from "./components/FooterSection.vue"; import FooterSection from "./components/FooterSection.vue";
import siteConfig from "./config/siteConfig"; import siteConfig from "./config/siteConfig";

View File

@@ -3,6 +3,10 @@
<p class="muted" v-if="showHitokoto && quote"> <p class="muted" v-if="showHitokoto && quote">
{{ quote }}<span v-if="from" class="from"> {{ from }}</span> {{ quote }}<span v-if="from" class="from"> {{ from }}</span>
</p> </p>
<p class="muted stats" v-if="showStats && !statsError">
👁 {{ visitors }} visitors · 📊 {{ pageviews }} pageviews
</p>
<!-- <p class="muted stats" v-if="showStats && statsError">🔒 由于启用了隐私保护拓展禁用状态统计</p> -->
<p class="muted beian" v-if="contact.beian"> <p class="muted beian" v-if="contact.beian">
<a :href="contact.beianLink || 'https://beian.miit.gov.cn/'" target="_blank" rel="noreferrer"> <a :href="contact.beianLink || 'https://beian.miit.gov.cn/'" target="_blank" rel="noreferrer">
{{ contact.beian }} {{ contact.beian }}
@@ -14,14 +18,19 @@
<script setup> <script setup>
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import siteConfig from "../config/siteConfig";
const props = defineProps({ contact: Object }); const props = defineProps({ contact: Object });
const quote = ref(""); const quote = ref("");
const from = ref(""); const from = ref("");
const showHitokoto = props.contact?.showHitokoto !== false; const pageviews = ref(0);
const visitors = ref(0);
const statsError = ref(true);
const showHitokoto = siteConfig.footer?.hitokoto?.enable;
const showStats = ref(siteConfig.umami?.enable);
const buildHitokotoUrl = () => { const buildHitokotoUrl = () => {
const type = siteConfig.footer?.hitokoto?.type;
const url = new URL("https://v1.hitokoto.cn/"); const url = new URL("https://v1.hitokoto.cn/");
const type = props.contact?.hitokotoType;
if (Array.isArray(type)) { if (Array.isArray(type)) {
type.filter(Boolean).forEach((t) => url.searchParams.append("c", t)); type.filter(Boolean).forEach((t) => url.searchParams.append("c", t));
} else if (typeof type === "string") { } else if (typeof type === "string") {
@@ -44,8 +53,52 @@ const fetchHitokoto = async () => {
} }
}; };
const fetchStats = async () => {
try {
if (!siteConfig.umami?.apiBase || !siteConfig.umami?.websiteId) {
return;
}
const apiBase = siteConfig.umami.apiBase;
const websiteId = siteConfig.umami.websiteId;
const apiKey = import.meta.env.VITE_UMAMI_API_KEY;
if (!apiKey) return;
// 获取统计数据
const endAt = Date.now();
const startAt = new Date(siteConfig.siteMeta.startDate).getTime();
const resp = await fetch(`${apiBase}/v1/websites/${websiteId}/stats?startAt=${startAt}&endAt=${endAt}`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
if (!resp.ok) {
console.warn(`Stats API returned ${resp.status}`);
statsError.value = true;
return;
}
const data = await resp.json();
if (data) {
statsError.value = false;
pageviews.value = data.pageviews;
visitors.value = data.visitors;
}
if (pageviews.value === 0 && visitors.value === 0) {
showStats.value = false;
}
} catch (e) {
statsError.value = true;
console.debug("Stats fetch failed (this is normal if blocked by ad blocker):", e.message);
}
};
onMounted(() => { onMounted(() => {
if (showHitokoto) fetchHitokoto(); if (showHitokoto) fetchHitokoto();
if (showStats.value) fetchStats();
}); });
</script> </script>
@@ -54,14 +107,17 @@ onMounted(() => {
text-align: center; text-align: center;
margin-top: 8px; margin-top: 8px;
} }
.from { .from {
margin-left: 6px; margin-left: 6px;
} }
.beian { .beian {
font-size: 12px; font-size: 12px;
margin: 6px 0; margin: 6px 0;
letter-spacing: 0.2px; letter-spacing: 0.2px;
} }
.beian a { .beian a {
color: inherit; color: inherit;
opacity: 0.85; opacity: 0.85;
@@ -69,12 +125,20 @@ onMounted(() => {
border-radius: 8px; border-radius: 8px;
transition: color 0.2s ease, background 0.2s ease, opacity 0.2s ease; transition: color 0.2s ease, background 0.2s ease, opacity 0.2s ease;
} }
.beian a:hover { .beian a:hover {
color: var(--accent, #7cc1ff); color: var(--accent, #7cc1ff);
background: rgba(124, 193, 255, 0.1); background: rgba(124, 193, 255, 0.1);
opacity: 1; opacity: 1;
} }
.custom-html { .custom-html {
margin-top: 6px; margin-top: 6px;
} }
.stats {
font-size: 12px;
margin: 6px 0;
letter-spacing: 0.2px;
}
</style> </style>

View File

@@ -1,60 +1,4 @@
interface SiteConfig { const siteConfig = {
profile: {
name: string;
title: string;
avatar: string;
bio: string;
birthday?: string;
gender?: string;
pronouns?: string;
location?: string;
};
socialLinks: Array<{
name: string;
url: string;
}>;
github: {
username: string;
};
about: Array<{
title: string;
desc: string;
icon: string;
}>;
siteMeta: {
title: string;
icon: string;
};
skills: Array<{
title: string;
items: string[];
}>;
sites: Array<{
name: string;
desc: string;
url: string;
}>;
projects: Array<{
name: string;
url: string;
desc: string;
}>;
friends: Array<{
name: string;
desc: string;
url: string;
avatar: string;
}>;
footer: {
beian: string;
beianLink: string;
showHitokoto: boolean;
hitokotoType: string;
customHtml: string;
};
}
const siteConfig: SiteConfig = {
profile: { profile: {
name: "RhenCloud", name: "RhenCloud",
title: "I'm RhenCloud.", title: "I'm RhenCloud.",
@@ -88,6 +32,14 @@ const siteConfig: SiteConfig = {
siteMeta: { siteMeta: {
title: "RhenCloud", title: "RhenCloud",
icon: "favicon.ico", // public/favicon.ico icon: "favicon.ico", // public/favicon.ico
startDate: "2025-12-06",
},
umami: {
enable: true,
url: "https://cloud.umami.is/script.js",
websiteId: "ddcd51c3-ccc7-45e4-81e6-11567027f69b",
apiBase: "https://api.umami.is",
}, },
skills: [ skills: [
@@ -143,9 +95,11 @@ const siteConfig: SiteConfig = {
footer: { footer: {
beian: "津ICP备2025039003号-1", beian: "津ICP备2025039003号-1",
beianLink: "https://beian.miit.gov.cn/", beianLink: "https://beian.miit.gov.cn/",
showHitokoto: true,
hitokotoType: "a&b&c&d&j",
customHtml: '<span style="opacity:.8">© 2025 <a href="https://rhen.cloud">RhenCloud</a></span>', customHtml: '<span style="opacity:.8">© 2025 <a href="https://rhen.cloud">RhenCloud</a></span>',
hitokoto: {
enable: true,
type: "a&b&c&d&j",
},
}, },
}; };

View File

@@ -1,6 +1,15 @@
import { createApp } from "vue"; import { createApp } from "vue";
import { VueUmamiPlugin } from "@jaseeey/vue-umami-plugin";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
import "./styles.css"; import "./styles.css";
import siteConfig from "./config/siteConfig";
createApp(App).use(router).mount("#app"); const app = createApp(App);
if (process.env.NODE_ENV !== "development") {
if (siteConfig.umami?.enable) {
app.use(VueUmamiPlugin({ websiteID: siteConfig.umami.websiteId, scriptSrc: siteConfig.umami.url, router }));
}
}
app.use(router).mount("#app");

View File

@@ -25,8 +25,6 @@ const github = reactive({
// 修改此处:使用 VITE_ 前缀 // 修改此处:使用 VITE_ 前缀
const githubToken = import.meta.env.VITE_GITHUB_TOKEN ?? ""; const githubToken = import.meta.env.VITE_GITHUB_TOKEN ?? "";
console.log(githubToken);
onMounted(() => { onMounted(() => {
document.title = siteMeta.title; document.title = siteMeta.title;
const link = document.querySelector("link[rel~='icon']") || document.createElement("link"); const link = document.querySelector("link[rel~='icon']") || document.createElement("link");