年龄
-- {{ age }} 岁 -
-关于我 · About Me
- -- {{ age }} 岁 -
-- {{ profile.gender }} -
-- {{ profile.pronouns }} -
-- {{ profile.location }} -
-{{ item.desc }}
-关于我 · About Me
+ ++ {{ age }} 岁 +
++ {{ profile.gender }} +
++ {{ profile.pronouns }} +
++ {{ profile.location }} +
+{{ item.desc }}
+欢迎互换友链 · Friends
-- {{ f.desc || "一个有趣的站点" }} -
- -欢迎互换友链 · Friends
++ {{ f.desc || "一个有趣的站点" }} +
+ +{{ dialogText }}
+
+ {{ exampleJson }}
+
+ 我的提交热力图 · Acitivity Heatmap
-我常用的语言 · Languages
-我的提交热力图 · Acitivity Heatmap
+我常用的语言 · Languages
+{{ profile.title }}
-{{ profile.bio }}
-{{ profile.title }}
+{{ profile.bio }}
+一些正在维护或已发布的项目 · Projects
-一些正在维护或已发布的项目 · Projects
+- {{ p.desc }} -
- -+ {{ p.desc }} +
+ +正在运行的站点 · Websites
- -- {{ site.desc }} -
- -正在运行的站点 · Websites
+ ++ {{ site.desc }} +
+ +我常用的工具与技术 · Skills & Technologies
-我常用的工具与技术 · Skills & Technologies
+社交账号 · Links
-社交账号 · Links
+我的提交热力图 · Activity Heatmap
-我常用的语言 · Languages
-- {{ wakatimeActiveTab === "weekly" ? "最近7天 · Last 7 Days" : "所有时间 · All Time" }} -
-语言使用统计 · Languages
-实时状态 · Current Status
-我的提交热力图 · Activity Heatmap
+我常用的语言 · Languages
++ {{ wakatimeActiveTab === "weekly" ? "最近7天 · Last 7 Days" : "所有时间 · All Time" }} +
+语言使用统计 · Languages
+实时状态 · Current Status
+{{ activeTab === "weekly" ? "最近7天 · Last 7 Days" : "所有时间 · All Time" }}
-语言使用统计 · Languages
-实时状态 · Current Status
-+ {{ activeTab === "weekly" ? "最近7天 · Last 7 Days" : "所有时间 · All Time" }} +
+语言使用统计 · Languages
+实时状态 · Current Status
+加载统计数据中...
-加载统计数据中...
+Giscus 未配置。
-在这里留下想说的话吧 · Comments
-Giscus 未配置。
+在这里留下想说的话吧 · Comments
+页面不见了,或已被移除。
- 返回首页 -页面不见了,或已被移除。
+ 返回首页 +一个新的友链申请已提交,以下是可直接复制到项目中的配置:
-
-${friendEntry}
-
- 申请者信息:
-名称:${ensureValue(name)}
-邮箱:${ensureValue(email)}
- -描述:${ensureValue(desc)}
-头像:${ensureValue(avatar)}
-想说的话:${ensureValue(message)}
-时间:${new Date().toISOString()}
- `; - - const info = await transporter.sendMail({ - from: senderEmail, - to: adminEmail, - replyTo: email, - subject: `友链申请 / 联系表单 · ${ensureValue(name)}`, - html: htmlMessage, - }); - - return { - message: "Mail sent", - id: info.messageId, - }; -}); +import { defineEventHandler, createError, readBody } from "h3"; +import nodemailer from "nodemailer"; +import type SMTPTransport from "nodemailer/lib/smtp-transport"; +import { useRuntimeConfig } from "#imports"; + +type MailConfig = { + smtpHost?: string; + smtpPort?: number | string; + smtpUser?: string; + smtpPass?: string; + senderEmail?: string; + adminEmail?: string; + smtpSecure?: boolean; +}; + +type SendMailPayload = { + name?: string; + url?: string; + desc?: string; + email?: string; + avatar?: string; + message?: string; +}; + +const ensureValue = (value?: string, fallback = "未填写") => + value?.trim() ? value.trim() : fallback; + +export default defineEventHandler(async (event) => { + const method = event.node.req.method; + if (method === "OPTIONS") { + event.node.res.statusCode = 200; + return { status: "ok" }; + } + + if (method !== "POST") { + throw createError({ statusCode: 405, statusMessage: "Method Not Allowed" }); + } + + const payload = (await readBody一个新的友链申请已提交,以下是可直接复制到项目中的配置:
+
+${friendEntry}
+
+ 申请者信息:
+名称:${ensureValue(name)}
+邮箱:${ensureValue(email)}
+ +描述:${ensureValue(desc)}
+头像:${ensureValue(avatar)}
+想说的话:${ensureValue(message)}
+时间:${new Date().toISOString()}
+ `; + + const info = await transporter.sendMail({ + from: senderEmail, + to: adminEmail, + replyTo: email, + subject: `友链申请 / 联系表单 · ${ensureValue(name)}`, + html: htmlMessage, + }); + + return { + message: "Mail sent", + id: info.messageId, + }; +}); diff --git a/server/api/wakatime.ts b/server/api/wakatime.ts index 9c97922..0e9f928 100644 --- a/server/api/wakatime.ts +++ b/server/api/wakatime.ts @@ -1,56 +1,56 @@ -import { defineEventHandler, getQuery, createError } from "h3"; -import { useRuntimeConfig } from "#imports"; - -export default defineEventHandler(async (event) => { - const res = event.node.res; - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type"); - - if (event.node.req.method === "OPTIONS") { - res.statusCode = 200; - return "ok"; - } - - if (event.node.req.method !== "GET") { - throw createError({ statusCode: 405, statusMessage: "Method Not Allowed" }); - } - - const config = useRuntimeConfig(); - const apiKey = config.wakatimeApiKey; - if (typeof apiKey !== "string") { - throw createError({ statusCode: 500, statusMessage: "Invalid WakaTime API Key configuration" }); - } - - const query = getQuery(event); - const apiUrl = (query.apiUrl as string) || config.wakatimeApiUrl; - - const headers = { - Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`, - }; - - try { - const [weeklyStatsResponse, allTimeStatsResponse, statusResponse] = await Promise.all([ - fetch(`${apiUrl}/users/current/stats/last_7_days`, { headers }), - fetch(`${apiUrl}/users/current/stats/all_time`, { headers }), - fetch(`${apiUrl}/users/current/status`, { headers }), - ]); - - if (!weeklyStatsResponse.ok) { - throw new Error(`Wakatime API error: ${weeklyStatsResponse.status}`); - } - - const weeklyStatsData = await weeklyStatsResponse.json(); - const allTimeStatsData = allTimeStatsResponse.ok ? await allTimeStatsResponse.json() : null; - const statusData = statusResponse.ok ? await statusResponse.json() : null; - - return { - weekly: weeklyStatsData.data, - allTime: allTimeStatsData ? allTimeStatsData.data : null, - status: statusData, - }; - } catch (error) { - console.error("Wakatime API error:", error); - throw createError({ statusCode: 500, statusMessage: "Failed to fetch Wakatime data" }); - } -}); +import { defineEventHandler, getQuery, createError } from "h3"; +import { useRuntimeConfig } from "#imports"; + +export default defineEventHandler(async (event) => { + const res = event.node.res; + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type"); + + if (event.node.req.method === "OPTIONS") { + res.statusCode = 200; + return "ok"; + } + + if (event.node.req.method !== "GET") { + throw createError({ statusCode: 405, statusMessage: "Method Not Allowed" }); + } + + const config = useRuntimeConfig(); + const apiKey = config.wakatimeApiKey; + if (typeof apiKey !== "string") { + throw createError({ statusCode: 500, statusMessage: "Invalid WakaTime API Key configuration" }); + } + + const query = getQuery(event); + const apiUrl = (query.apiUrl as string) || config.wakatimeApiUrl; + + const headers = { + Authorization: `Basic ${Buffer.from(apiKey).toString("base64")}`, + }; + + try { + const [weeklyStatsResponse, allTimeStatsResponse, statusResponse] = await Promise.all([ + fetch(`${apiUrl}/users/current/stats/last_7_days`, { headers }), + fetch(`${apiUrl}/users/current/stats/all_time`, { headers }), + fetch(`${apiUrl}/users/current/status`, { headers }), + ]); + + if (!weeklyStatsResponse.ok) { + throw new Error(`Wakatime API error: ${weeklyStatsResponse.status}`); + } + + const weeklyStatsData = await weeklyStatsResponse.json(); + const allTimeStatsData = allTimeStatsResponse.ok ? await allTimeStatsResponse.json() : null; + const statusData = statusResponse.ok ? await statusResponse.json() : null; + + return { + weekly: weeklyStatsData.data, + allTime: allTimeStatsData ? allTimeStatsData.data : null, + status: statusData, + }; + } catch (error) { + console.error("Wakatime API error:", error); + throw createError({ statusCode: 500, statusMessage: "Failed to fetch Wakatime data" }); + } +}); diff --git a/tailwind.config.ts b/tailwind.config.ts index b497129..6a819d1 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,39 +1,51 @@ -import type { Config } from "tailwindcss"; - -export default { - content: ["./app/**/*.{vue,js,ts}", "./app/components/**/*.vue", "./app/pages/**/*.vue", "./app/layouts/**/*.vue"], - theme: { - extend: { - colors: { - // 自定义颜色变量(对应现有的 CSS 变量) - primary: "rgb(124, 193, 255)", - accent: "rgb(124, 193, 255)", - "surface-primary": "rgb(15, 22, 41)", - "surface-secondary": "rgb(27, 43, 75)", - "text-primary": "rgb(232, 238, 252)", - "text-secondary": "rgb(159, 172, 200)", - "text-muted": "rgb(104, 120, 152)", - }, - fontFamily: { - sans: ['"Inter"', "system-ui", "-apple-system", "BlinkMacSystemFont", '"Segoe UI"', "sans-serif"], - }, - spacing: { - "safe-x": "max(1rem, env(safe-area-inset-left))", - "safe-y": "max(1rem, env(safe-area-inset-top))", - }, - boxShadow: { - "sm-dark": "0 4px 12px rgba(0, 0, 0, 0.15)", - "md-dark": "0 8px 24px rgba(0, 0, 0, 0.18)", - "lg-dark": "0 12px 32px rgba(0, 0, 0, 0.22)", - "xl-dark": "0 16px 48px rgba(0, 0, 0, 0.25)", - }, - backgroundImage: { - "gradient-dark": "radial-gradient(circle at 20% 20%, #1b2b4b, #0f1629)", - }, - backdropBlur: { - xs: "2px", - }, - }, - }, - plugins: [], -} satisfies Config; +import type { Config } from "tailwindcss"; + +export default { + content: [ + "./app/**/*.{vue,js,ts}", + "./app/components/**/*.vue", + "./app/pages/**/*.vue", + "./app/layouts/**/*.vue", + ], + theme: { + extend: { + colors: { + // 自定义颜色变量(对应现有的 CSS 变量) + primary: "rgb(124, 193, 255)", + accent: "rgb(124, 193, 255)", + "surface-primary": "rgb(15, 22, 41)", + "surface-secondary": "rgb(27, 43, 75)", + "text-primary": "rgb(232, 238, 252)", + "text-secondary": "rgb(159, 172, 200)", + "text-muted": "rgb(104, 120, 152)", + }, + fontFamily: { + sans: [ + '"Inter"', + "system-ui", + "-apple-system", + "BlinkMacSystemFont", + '"Segoe UI"', + "sans-serif", + ], + }, + spacing: { + "safe-x": "max(1rem, env(safe-area-inset-left))", + "safe-y": "max(1rem, env(safe-area-inset-top))", + }, + boxShadow: { + "sm-dark": "0 4px 12px rgba(0, 0, 0, 0.15)", + "md-dark": "0 8px 24px rgba(0, 0, 0, 0.18)", + "lg-dark": "0 12px 32px rgba(0, 0, 0, 0.22)", + "xl-dark": "0 16px 48px rgba(0, 0, 0, 0.25)", + }, + backgroundImage: { + "gradient-dark": "radial-gradient(circle at 20% 20%, #1b2b4b, #0f1629)", + }, + backdropBlur: { + xs: "2px", + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/tsconfig.json b/tsconfig.json index 9e2c134..8ea5781 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,10 @@ -{ - "extends": "./.nuxt/tsconfig.json", - "compilerOptions": { - "types": ["node"] - }, - "vueCompilerOptions": { - "globalTypesPath": "./node_modules/.vue-global-types" - }, - "include": ["nuxt.config.ts", "app/**/*.ts", "app/**/*.vue", "app/**/*.d.ts", "server/**/*.ts"] -} +{ + "extends": "./.nuxt/tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "vueCompilerOptions": { + "globalTypesPath": "./node_modules/.vue-global-types" + }, + "include": ["nuxt.config.ts", "app/**/*.ts", "app/**/*.vue", "app/**/*.d.ts", "server/**/*.ts"] +} diff --git a/tsconfig.node.json b/tsconfig.node.json index d02ef0f..4bb1b7d 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,11 +1,11 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "types": ["node"] - }, - "include": ["nuxt.config.ts"] -} +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "types": ["node"] + }, + "include": ["nuxt.config.ts"] +} diff --git a/vercel.json b/vercel.json index 3956341..d1b45a3 100644 --- a/vercel.json +++ b/vercel.json @@ -1,4 +1,4 @@ -{ - "version": 2, - "framework": "nuxtjs" -} \ No newline at end of file +{ + "version": 2, + "framework": "nuxtjs" +}