Compare commits

..

3 Commits

9 changed files with 4643 additions and 246 deletions

View File

@@ -9,14 +9,7 @@
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/10 backdrop-blur-sm border border-white/10 text-text-primary text-sm font-medium transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary hover:-translate-y-1" class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/10 backdrop-blur-sm border border-white/10 text-text-primary text-sm font-medium transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary hover:-translate-y-1"
> >
<span v-if="iconFor(link)" class="inline-flex items-center justify-center w-5 h-5"> <span v-if="iconFor(link)" class="inline-flex items-center justify-center w-5 h-5">
<i v-if="iconFor(link).fa" :class="iconFor(link).fa" /> <Icon v-if="iconFor(link).name" :name="iconFor(link).name" width="20" height="20" />
<NuxtImg
v-else
:src="iconFor(link).src"
:alt="link.name"
loading="lazy"
class="w-full h-full"
/>
</span> </span>
<span>{{ link.name }}</span> <span>{{ link.name }}</span>
</NuxtLink> </NuxtLink>
@@ -26,7 +19,6 @@
</template> </template>
<script setup> <script setup>
import { onMounted } from "vue";
defineProps({ defineProps({
links: { links: {
type: Array, type: Array,
@@ -35,44 +27,31 @@ defineProps({
}); });
const iconMap = { const iconMap = {
bilibili: "fa-brands fa-bilibili", bilibili: "simple-icons:bilibili",
github: "fa-brands fa-github", github: "simple-icons:github",
blog: "fa-solid fa-rss", blog: "fa6-solid:book",
email: "fa-solid fa-envelope", email: "fa6-solid:envelope",
mail: "fa-solid fa-envelope", mail: "fa6-solid:envelope",
telegram: "fa-brands fa-telegram", telegram: "simple-icons:telegram",
twitter: "fa-brands fa-x-twitter", twitter: "simple-icons:twitter",
x: "fa-brands fa-x-twitter", x: "simple-icons:x",
linkedin: "fa-brands fa-linkedin", linkedin: "simple-icons:linkedin",
youtube: "fa-brands fa-youtube", youtube: "simple-icons:youtube",
facebook: "fa-brands fa-facebook", facebook: "simple-icons:facebook",
instagram: "fa-brands fa-instagram", instagram: "simple-icons:instagram",
reddit: "fa-brands fa-reddit", reddit: "simple-icons:reddit",
discord: "fa-brands fa-discord", discord: "simple-icons:discord",
weibo: "fa-brands fa-weibo", weibo: "simple-icons:sinaweibo",
zhihu: "fa-brands fa-zhihu", zhihu: "simple-icons:zhihu",
wechat: "fa-brands fa-weixin", wechat: "simple-icons:wechat",
weixin: "fa-brands fa-weixin", weixin: "simple-icons:wechat",
qq: "fa-brands fa-qq", qq: "simple-icons:qq",
}; };
const iconFor = (link) => { const iconFor = (link) => {
const key = (link.name || "").toLowerCase(); const key = (link.name || "").toLowerCase();
if (iconMap[key]) return { fa: iconMap[key] }; if (iconMap[key]) return { name: iconMap[key] };
if (link.icon) return { src: link.icon }; if (link.icon) return { src: link.icon };
return null; return null;
}; };
onMounted(() => {
const id = "fa-cdn";
if (document.getElementById(id)) return;
const link = document.createElement("link");
link.id = id;
link.rel = "stylesheet";
link.href =
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css?font-display=swap";
link.crossOrigin = "anonymous";
link.referrerPolicy = "no-referrer";
document.head.appendChild(link);
});
</script> </script>

View File

@@ -79,15 +79,9 @@
<script setup> <script setup>
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed } from "vue";
import siteConfig from "~/config/siteConfig";
const props = defineProps({ const wakapi = siteConfig.wakapi;
wakatime: {
type: Object,
required: false,
default: () => ({ languages: [] }),
},
});
const wakatime = props.wakatime;
const weeklyData = ref(null); const weeklyData = ref(null);
const allTimeData = ref(null); const allTimeData = ref(null);
@@ -123,65 +117,45 @@ const formatTime = (seconds) => {
}; };
const fetchWakatimeData = async () => { const fetchWakatimeData = async () => {
if (!wakatime.enable) return; if (!wakapi.enable) {
console.warn("Wakatime is disabled in siteConfig.");
return;
}
const apiUrl = wakapi.apiUrl || "https://wakatime.com/api/v1";
const username = wakapi.username;
if (!username) {
console.error("Wakatime username is not configured.");
return;
}
try { try {
const params = new URLSearchParams(); const [weeklyStatsResponse, allTimeStatsResponse, statusResponse] = await Promise.all([
if (wakatime.apiUrl && wakatime.apiUrl !== "https://wakatime.com/api/v1") { fetch(`${apiUrl}/users/${username}/stats/last_7_days`),
params.append("apiUrl", wakatime.apiUrl); fetch(`${apiUrl}/users/${username}/stats`),
} fetch(`${apiUrl}/users/${username}/status`),
const url = `/api/wakatime${params.toString() ? `?${params.toString()}` : ""}`; ]);
console.log("Fetching Wakatime data from:", url);
const response = await fetch(url);
console.log("Response status:", response.status);
console.log("Response headers:", Object.fromEntries(response.headers.entries()));
if (response.ok) { if (weeklyStatsResponse.ok) {
const data = await response.json(); weeklyData.value = await weeklyStatsResponse.json();
console.log("Wakatime data:", data);
weeklyData.value = data.weekly;
allTimeData.value = data.allTime;
statusData.value = data.status;
} else { } else {
const errorText = await response.text(); console.error("Failed to fetch weekly stats:", weeklyStatsResponse.status);
console.error("API Error:", response.status, errorText); }
if (response.status === 500 && errorText.includes("Wakatime API Key not configured")) {
console.warn("Wakatime API Key not configured - hiding component"); if (allTimeStatsResponse.ok) {
showComponent.value = false; allTimeData.value = await allTimeStatsResponse.json();
return; } else {
} console.warn("All-time stats not available:", allTimeStatsResponse.status);
throw new Error(`API returned ${response.status}: ${errorText}`); }
if (statusResponse.ok) {
statusData.value = await statusResponse.json();
} else {
console.warn("Status data not available:", statusResponse.status);
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch Wakatime data:", error); console.error("Error fetching Wakatime data:", error);
// 在开发环境中,如果 API 不可用,设置一些示例数据
if (import.meta.env.DEV) {
console.log("Using mock data for development");
weeklyData.value = {
total_seconds: 36000,
daily_average: 5142,
days_including_holidays: 7,
languages: [
{ name: "TypeScript", percent: 45.2, total_seconds: 16272 },
{ name: "Vue", percent: 30.1, total_seconds: 10836 },
{ name: "JavaScript", percent: 15.3, total_seconds: 5508 },
{ name: "Python", percent: 9.4, total_seconds: 3384 },
],
};
allTimeData.value = {
total_seconds: 864000,
daily_average: 2800,
days_including_holidays: 308,
languages: [
{ name: "JavaScript", percent: 35.2, total_seconds: 304128 },
{ name: "TypeScript", percent: 28.1, total_seconds: 242688 },
{ name: "Python", percent: 20.3, total_seconds: 175392 },
{ name: "Vue", percent: 10.1, total_seconds: 87296 },
{ name: "CSS", percent: 6.3, total_seconds: 54432 },
],
};
statusData.value = { is_coding: false };
}
} }
}; };

View File

@@ -2,7 +2,7 @@ const siteConfig = {
profile: { profile: {
name: "RhenCloud", name: "RhenCloud",
title: "I'm RhenCloud.", title: "I'm RhenCloud.",
avatar: "/avatar.webp", // public/avatar.webp avatar: "/avatar-1.webp", // public/avatar.webp
bio: "趁世界还未重启之前 约一次爱恋", bio: "趁世界还未重启之前 约一次爱恋",
email: "i@rhen.cloud", email: "i@rhen.cloud",
birthday: "2010-03-28", birthday: "2010-03-28",
@@ -17,6 +17,7 @@ const siteConfig = {
{ name: "Telegram", url: "https://t.me/RhenCloud" }, { name: "Telegram", url: "https://t.me/RhenCloud" },
{ name: "Bilibili", url: "https://space.bilibili.com/1502883335" }, { name: "Bilibili", url: "https://space.bilibili.com/1502883335" },
{ name: "Blog", url: "https://blog.rhen.cloud" }, { name: "Blog", url: "https://blog.rhen.cloud" },
{ name: "Twitter", url: "https://x.com/RhenCloud75" },
], ],
github: { github: {
@@ -32,9 +33,13 @@ const siteConfig = {
siteMeta: { siteMeta: {
title: "RhenCloud", title: "RhenCloud",
description: "RhenCloud的个人主页分享技术、生活、兴趣。",
keywords: ["Technology", "Blog", "Development", "Programming"],
author: "RhenCloud",
url: "https://rhen.cloud", url: "https://rhen.cloud",
icon: "/favicon.svg", // public/favicon.svg favicon: "/favicon.svg", // public/favicon.svg
startDate: "2025-12-06", startDate: "2025-12-06",
lang: "zh-CN",
}, },
appearance: { appearance: {
@@ -80,9 +85,10 @@ const siteConfig = {
apiBase: "https://api.umami.is", apiBase: "https://api.umami.is",
}, },
wakatime: { wakapi: {
enable: true, enable: false,
apiUrl: "https://wakapi.rhen.cloud/api/v1", apiUrl: "https://wakapi.rhen.cloud/api/v1",
username: "RhenCloud",
}, },
skills: [ skills: [

2812
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,18 +5,7 @@ import tailwindcss from "@tailwindcss/vite";
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: "2025-12-12", compatibilityDate: "2025-12-12",
srcDir: "app/", srcDir: "app/",
modules: ["@nuxt/image", "@nuxt/eslint", "@nuxtjs/sitemap"], modules: ["@nuxt/image", "@nuxt/eslint", "@nuxtjs/sitemap", "@nuxt/icon", "@nuxtjs/seo"],
// eslint: {
// config: {
// extends: ["plugin:nuxt/recommended", "prettier"],
// plugins: ["prettier"],
// rules: {
// "prettier/prettier": "error",
// },
// stylistic: true
// },
// },
// 禁用 Vue Router 的非关键警告 // 禁用 Vue Router 的非关键警告
vue: { vue: {
@@ -32,11 +21,6 @@ export default defineNuxtConfig({
plugins: [tailwindcss()], plugins: [tailwindcss()],
}, },
site: {
url: siteConfig.siteMeta.url,
title: siteConfig.siteMeta.title,
},
routeRules: { routeRules: {
"/": { prerender: true }, "/": { prerender: true },
"/about": { isr: 3600 }, "/about": { isr: 3600 },
@@ -47,32 +31,26 @@ export default defineNuxtConfig({
app: { app: {
head: { head: {
charset: "utf-8",
viewport: "width=device-width,initial-scale=1,maximum-scale=5",
title: siteConfig.siteMeta.title, title: siteConfig.siteMeta.title,
titleTemplate: `%s - ${siteConfig.siteMeta.title}`,
meta: [
{ name: "author", content: siteConfig.siteMeta.author },
{ name: "language", content: "zh-CN" },
{ name: "description", content: siteConfig.siteMeta.description },
],
link: [ link: [
{ rel: "icon", href: siteConfig.siteMeta.icon }, { rel: "icon", href: siteConfig.siteMeta.favicon, type: "image/svg+xml" },
// Font Awesome CDN 预加载和优化 { rel: "canonical", href: siteConfig.siteMeta.url },
{ { rel: "alternate", hreflang: siteConfig.siteMeta.lang, href: siteConfig.siteMeta.url },
rel: "preload", { rel: "dns-prefetch", href: siteConfig.siteMeta.url },
as: "style", { rel: "preconnect", href: siteConfig.siteMeta.url },
href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css?font-display=swap", { rel: "icon", href: siteConfig.siteMeta.favicon },
crossorigin: "anonymous",
},
{
rel: "preload",
as: "font",
href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/webfonts/fa-solid-900.woff2?font-display=swap",
type: "font/woff2",
crossorigin: "anonymous",
},
{
rel: "preload",
as: "font",
href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/webfonts/fa-brands-400.woff2?font-display=swap",
type: "font/woff2",
crossorigin: "anonymous",
},
], ],
}, },
pageTransition: { name: "page", mode: "out-in" },
layoutTransition: { name: "layout", mode: "out-in" },
}, },
nitro: { nitro: {
@@ -91,9 +69,11 @@ export default defineNuxtConfig({
senderEmail: process.env.SENDER_EMAIL ?? "", senderEmail: process.env.SENDER_EMAIL ?? "",
adminEmail: process.env.ADMIN_EMAIL ?? "", adminEmail: process.env.ADMIN_EMAIL ?? "",
smtpSecure: process.env.SMTP_SECURE ? process.env.SMTP_SECURE === "true" : undefined, smtpSecure: process.env.SMTP_SECURE ? process.env.SMTP_SECURE === "true" : undefined,
wakatimeApiKey: process.env.WAKATIME_API_KEY ?? "",
wakatimeApiUrl: process.env.WAKATIME_API_URL ?? "https://wakatime.com/api/v1",
githubToken: process.env.NUXT_PUBLIC_GITHUB_TOKEN ?? "", githubToken: process.env.NUXT_PUBLIC_GITHUB_TOKEN ?? "",
umamiApiKey: process.env.UMAMI_API_KEY ?? "", umamiApiKey: process.env.UMAMI_API_KEY ?? "",
public: {
wakatimeApiKey: process.env.WAKATIME_API_KEY ?? "",
wakatimeApiUrl: process.env.WAKATIME_API_URL ?? "https://wakatime.com/api/v1",
},
}, },
}); });

View File

@@ -1,6 +1,11 @@
{ {
"name": "cloud-home", "name": "cloud-home",
"private": false, "author": {
"name": "RhenCloud",
"email": "i@rhen.cloud",
"url": "https://rhen.cloud"
},
"private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "nuxt dev", "dev": "nuxt dev",
@@ -15,23 +20,27 @@
"dependencies": { "dependencies": {
"@giscus/vue": "^3.1.1", "@giscus/vue": "^3.1.1",
"@jaseeey/vue-umami-plugin": "^1.4.0", "@jaseeey/vue-umami-plugin": "^1.4.0",
"@nuxt/icon": "^2.2.0",
"@nuxt/image": "2.0.0", "@nuxt/image": "2.0.0",
"@nuxtjs/sitemap": "^7.5.0", "@nuxtjs/icon": "^2.6.0",
"nodemailer": "^7.0.11", "@nuxtjs/seo": "3.3.0",
"@nuxtjs/sitemap": "^7.5.2",
"nodemailer": "^7.0.12",
"nuxt": "^4.2.2", "nuxt": "^4.2.2",
"vite-tsconfig-paths": "^6.0.1" "vite-tsconfig-paths": "^6.0.4"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/fa6-brands": "^1.2.6",
"@iconify-json/simple-icons": "^1.2.66",
"@nuxt/eslint": "1.12.1", "@nuxt/eslint": "1.12.1",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@types/node": "^24.10.1", "@types/nodemailer": "^7.0.5",
"@types/nodemailer": "^7.0.4", "@typescript-eslint/parser": "^8.53.0",
"@typescript-eslint/parser": "^8.50.0", "autoprefixer": "^10.4.23",
"autoprefixer": "^10.4.22",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.5",
"prettier": "^3.7.4", "prettier": "^3.8.0",
"prettier-eslint": "^16.4.2", "prettier-eslint": "^16.4.2",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "^5.9.3" "typescript": "^5.9.3"

1808
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,2 @@
onlyBuiltDependencies: ignoredBuiltDependencies:
- esbuild - core-js
- sharp@0.34.5

BIN
public/avatar-1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB