mirror of
https://github.com/RhenCloud/Cloud-Home.git
synced 2026-01-22 17:39:07 +08:00
feat: 添加 Umami 分析支持,更新配置文档
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
11
src/main.ts
11
src/main.ts
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user