From b5b55093209914b1b1048485003f6c8139684a99 Mon Sep 17 00:00:00 2001 From: RhenCloud Date: Sun, 7 Dec 2025 23:20:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Wakatime=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E4=BB=A5=E6=98=BE=E7=A4=BA=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 + api/wakatime.ts | 71 +++++ src/components/GithubSection.vue | 10 +- src/components/StatsSection.vue | 432 +++++++++++++++++++++++++++++ src/components/WakatimeSection.vue | 333 ++++++++++++++++++++++ src/config/siteConfig.ts | 5 + src/views/About.vue | 7 +- 7 files changed, 856 insertions(+), 8 deletions(-) create mode 100644 api/wakatime.ts create mode 100644 src/components/StatsSection.vue create mode 100644 src/components/WakatimeSection.vue diff --git a/README.md b/README.md index a181d55..60c463f 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,11 @@ const siteConfig: SiteConfig = { apiBase: "https://api.umami.is", // Umami API 地址,一般无需修改 }, + wakatime: { + enable: true, // 是否启用 Wakatime 统计 + apiUrl: "https://wakatime.com/api/v1", // Wakatime API 地址,默认官方地址 + }, + // 技能图标展示,详见https://github.com/tandpfun/skill-icons#icons-list skills: [ { title: "前端", items: ["css", "html", "javascript", "typescript", "vue"] }, @@ -163,6 +168,7 @@ const siteConfig: SiteConfig = { - `VITE_GITHUB_TOKEN`: 具有仓库读取权限的 GitHub Token,用于绕过 GitHub API 速率限制。(可选) - `UMAMI_API_KEY`: Umami 分析的 API Key。 +- `WAKATIME_API_KEY`: Wakatime API Key,用于获取编码统计数据。 - `SMTP_HOST`: 邮件服务器主机名 - `SMTP_PORT`: 端口(如 465 或 587) - `SMTP_USER`: 发件人邮箱账号 diff --git a/api/wakatime.ts b/api/wakatime.ts new file mode 100644 index 0000000..1a03691 --- /dev/null +++ b/api/wakatime.ts @@ -0,0 +1,71 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; + +export default async function handler(req: VercelRequest, res: VercelResponse) { + // CORS 处理 + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type"); + + if (req.method === "OPTIONS") { + return res.status(200).end(); + } + + if (req.method !== "GET") { + return res.status(405).json({ error: "Method Not Allowed" }); + } + + const apiKey = process.env.WAKATIME_API_KEY; + const defaultApiUrl = process.env.WAKATIME_API_URL || "https://wakatime.com/api/v1"; + const apiUrl = (req.query.apiUrl as string) || defaultApiUrl; + if (!apiKey) { + return res.status(500).json({ error: "Wakatime API Key not configured" }); + } + + try { + // 获取一周统计数据 + const weeklyStatsResponse = await fetch(`${apiUrl}/users/current/stats/last_7_days`, { + headers: { + Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`, + }, + }); + + if (!weeklyStatsResponse.ok) { + throw new Error(`Wakatime API error: ${weeklyStatsResponse.status}`); + } + + const weeklyStatsData = await weeklyStatsResponse.json(); + + // 获取所有时间统计数据 + const allTimeStatsResponse = await fetch(`${apiUrl}/users/current/stats/all_time`, { + headers: { + Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`, + }, + }); + + let allTimeStatsData = null; + if (allTimeStatsResponse.ok) { + allTimeStatsData = await allTimeStatsResponse.json(); + } + + // 获取当前用户状态 + const statusResponse = await fetch(`${apiUrl}/users/current/status`, { + headers: { + Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`, + }, + }); + + let statusData = null; + if (statusResponse.ok) { + statusData = await statusResponse.json(); + } + + res.status(200).json({ + weekly: weeklyStatsData.data, + allTime: allTimeStatsData ? allTimeStatsData.data : null, + status: statusData, + }); + } catch (error) { + console.error("Wakatime API error:", error); + res.status(500).json({ error: "Failed to fetch Wakatime data" }); + } +} diff --git a/src/components/GithubSection.vue b/src/components/GithubSection.vue index b8caa2a..3610284 100644 --- a/src/components/GithubSection.vue +++ b/src/components/GithubSection.vue @@ -1,6 +1,11 @@ diff --git a/src/components/StatsSection.vue b/src/components/StatsSection.vue new file mode 100644 index 0000000..281d944 --- /dev/null +++ b/src/components/StatsSection.vue @@ -0,0 +1,432 @@ + + + + + diff --git a/src/components/WakatimeSection.vue b/src/components/WakatimeSection.vue new file mode 100644 index 0000000..0896dbf --- /dev/null +++ b/src/components/WakatimeSection.vue @@ -0,0 +1,333 @@ + + + + + diff --git a/src/config/siteConfig.ts b/src/config/siteConfig.ts index 45c20b4..10c2d80 100644 --- a/src/config/siteConfig.ts +++ b/src/config/siteConfig.ts @@ -74,6 +74,11 @@ const siteConfig = { apiBase: "https://api.umami.is", }, + wakatime: { + enable: true, + apiUrl: "https://wakapi.rhen.cloud/api/v1", + }, + skills: [ { title: "前端", items: ["css", "html", "javascript", "typescript", "vue"] }, { title: "后端 / 云", items: ["cpp", "cloudflare", "docker", "java", "mysql", "nodejs", "python", "vercel"] }, diff --git a/src/views/About.vue b/src/views/About.vue index 589a0ab..bcd0747 100644 --- a/src/views/About.vue +++ b/src/views/About.vue @@ -2,7 +2,7 @@
- +
@@ -10,12 +10,13 @@ import { onMounted, reactive } from "vue"; import HeroSection from "../components/HeroSection.vue"; import SkillsSection from "../components/SkillsSection.vue"; -import GithubSection from "../components/GithubSection.vue"; +import StatsSection from "../components/StatsSection.vue"; import siteConfig from "../config/siteConfig"; const profile = siteConfig.profile; const siteMeta = siteConfig.siteMeta; const skills = siteConfig.skills; +const wakatime = siteConfig.wakatime; const github = reactive({ ...siteConfig.github, @@ -51,7 +52,7 @@ async function fetchGithubMeta() { const parsed = Object.entries(counts) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count) - .slice(0, 4); + .slice(0, 5); github.languages = parsed.map((item) => ({ name: item.name, percent: Math.round((item.count / total) * 100),