diff --git a/README.md b/README.md index 6783822..8eabc54 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Cloud Home -一个基于 Nuxt 3 (Vue 3) 的个人主页模板,内置友链申请、网站展示、项目展示、友链随机展示、自定义配置,支持 Vercel 部署与邮件通知。 +一款基于 Nuxt 4 的个人主页模板,内置友链申请、网站展示、项目展示、友链随机展示、自定义配置,支持 Vercel 部署与邮件通知。 ## 特性 @@ -11,10 +11,16 @@ ## 技术栈 -- 前端:Nuxt 3(Vue 3)+ HTML + CSS -- 构建 / 运行:Nuxt 3 + Nitro +- 前端:Nuxt 4 + Tailwind CSS +- 构建 / 运行:Nuxt 4 + Nitro - 部署:Vercel(Nuxt 构建 + Nitro 函数) +## TODO + +- [ ] 增加主题色配置 +- [ ] 增加追番模块 +- [ ] 增加留言板模块 + ## 致谢 排名不分先后 diff --git a/src/app.vue b/app/app.vue similarity index 91% rename from src/app.vue rename to app/app.vue index 669ae64..348dce4 100644 --- a/src/app.vue +++ b/app/app.vue @@ -46,18 +46,12 @@ const checkIfMobile = () => { onMounted(() => { checkIfMobile(); window.addEventListener("resize", checkIfMobile); - // const script = document.createElement("script"); - // script.src = "/js/netease-mini-player-v2.js"; - // document.head.appendChild(script); }); const getBackgroundImage = () => { if (!bg.enable) return undefined; - const image = isMobile.value && bg.mobileImage ? bg.mobileImage : bg.image; - if (!image) return undefined; - return image.startsWith("http") ? image : `/${image}`; }; @@ -65,9 +59,7 @@ const backgroundStyle = computed(() => ({ backgroundColor: "#0f1629" })); const overlayStyle = computed(() => { const imageUrl = getBackgroundImage(); - if (!bg.enable || !imageUrl) return {}; - return { backgroundImage: `linear-gradient(${bg.overlay}, ${bg.overlay}), url('${imageUrl}')`, backgroundSize: "cover", diff --git a/app/components/AboutSection.vue b/app/components/AboutSection.vue new file mode 100644 index 0000000..32f2836 --- /dev/null +++ b/app/components/AboutSection.vue @@ -0,0 +1,84 @@ + + + diff --git a/src/components/FooterSection.vue b/app/components/FooterSection.vue similarity index 64% rename from src/components/FooterSection.vue rename to app/components/FooterSection.vue index 0c32047..70cfdd9 100644 --- a/src/components/FooterSection.vue +++ b/app/components/FooterSection.vue @@ -1,18 +1,38 @@ @@ -103,45 +123,3 @@ onMounted(() => { if (showStats.value) fetchStats(); }); - - diff --git a/app/components/FriendsSection.vue b/app/components/FriendsSection.vue new file mode 100644 index 0000000..d59c47a --- /dev/null +++ b/app/components/FriendsSection.vue @@ -0,0 +1,225 @@ + + + diff --git a/app/components/GithubSection.vue b/app/components/GithubSection.vue new file mode 100644 index 0000000..9ed8311 --- /dev/null +++ b/app/components/GithubSection.vue @@ -0,0 +1,50 @@ + + + diff --git a/app/components/HeroSection.vue b/app/components/HeroSection.vue new file mode 100644 index 0000000..97f5d8e --- /dev/null +++ b/app/components/HeroSection.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/MusicPlayer.vue b/app/components/MusicPlayer.vue similarity index 53% rename from src/components/MusicPlayer.vue rename to app/components/MusicPlayer.vue index 6f1dbfd..90a61c6 100644 --- a/src/components/MusicPlayer.vue +++ b/app/components/MusicPlayer.vue @@ -1,18 +1,10 @@ diff --git a/app/components/ProjectsSection.vue b/app/components/ProjectsSection.vue new file mode 100644 index 0000000..7f46528 --- /dev/null +++ b/app/components/ProjectsSection.vue @@ -0,0 +1,35 @@ + + + diff --git a/app/components/SitesSection.vue b/app/components/SitesSection.vue new file mode 100644 index 0000000..b143af8 --- /dev/null +++ b/app/components/SitesSection.vue @@ -0,0 +1,35 @@ + + + diff --git a/app/components/SitesSection_old.vue b/app/components/SitesSection_old.vue new file mode 100644 index 0000000..5965146 --- /dev/null +++ b/app/components/SitesSection_old.vue @@ -0,0 +1,34 @@ + + + diff --git a/app/components/SkillsSection.vue b/app/components/SkillsSection.vue new file mode 100644 index 0000000..525e972 --- /dev/null +++ b/app/components/SkillsSection.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/SocialLinks.vue b/app/components/SocialLinks.vue similarity index 65% rename from src/components/SocialLinks.vue rename to app/components/SocialLinks.vue index 65596ae..2ac6b22 100644 --- a/src/components/SocialLinks.vue +++ b/app/components/SocialLinks.vue @@ -1,12 +1,13 @@ - - - - - \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index f76f0f8..2f26bb5 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,22 +1,57 @@ import { defineNuxtConfig } from "nuxt/config"; -import siteConfig from "./src/config/siteConfig"; +import siteConfig from "./app/config/siteConfig"; +import tailwindcss from "@tailwindcss/vite"; export default defineNuxtConfig({ compatibilityDate: "2025-12-12", - srcDir: "src/", - css: ["~/styles.css"], + srcDir: "app/", + // 禁用 Vue Router 的非关键警告 + vue: { + compilerOptions: { + isCustomElement: (tag) => tag.startsWith("ion-"), + }, + }, + // Tailwind CSS 集成 + css: ["~/styles.global.css"], + vite: { + plugins: [tailwindcss()], + }, app: { head: { title: siteConfig.siteMeta.title, - link: [{ rel: "icon", href: siteConfig.siteMeta.icon }], + link: [ + { rel: "icon", href: siteConfig.siteMeta.icon }, + // Font Awesome CDN 预加载和优化 + { + rel: "preload", + as: "style", + href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css", + crossorigin: "anonymous", + }, + { + rel: "preload", + as: "font", + href: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/webfonts/fa-solid-900.woff2", + 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", + type: "font/woff2", + crossorigin: "anonymous", + }, + ], }, }, - // nitro: { - // prerender: { - // crawlLinks: true, - // routes: ["/sitemap.xml", "/rss.xml"], - // }, - // }, + nitro: { + prerender: { + crawlLinks: true, + // routes: ["/sitemap.xml", "/rss.xml"], + }, + minify: true, + }, runtimeConfig: { smtpHost: process.env.SMTP_HOST ?? "", smtpPort: Number(process.env.SMTP_PORT ?? 465), diff --git a/package.json b/package.json index aeeac6a..bdbe849 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,15 @@ "dependencies": { "@jaseeey/vue-umami-plugin": "^1.4.0", "nodemailer": "^7.0.11", - "nuxt": "^4.2.2" + "nuxt": "^4.2.2", + "vite-tsconfig-paths": "^6.0.1" }, "devDependencies": { + "@tailwindcss/vite": "^4.1.18", "@types/node": "^24.10.1", "@types/nodemailer": "^7.0.4", + "autoprefixer": "^10.4.22", + "tailwindcss": "^4.1.18", "typescript": "^5.9.3" } } \ No newline at end of file diff --git a/public/avatar.webp b/public/avatar.webp index 1d61fad..6d81fa5 100644 Binary files a/public/avatar.webp and b/public/avatar.webp differ diff --git a/public/background.png b/public/background.png deleted file mode 100644 index 143f71d..0000000 Binary files a/public/background.png and /dev/null differ diff --git a/public/background.webp b/public/background.webp new file mode 100644 index 0000000..7c8ea69 Binary files /dev/null and b/public/background.webp differ diff --git a/src/components/AboutSection.vue b/src/components/AboutSection.vue deleted file mode 100644 index fc69095..0000000 --- a/src/components/AboutSection.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - diff --git a/src/components/FriendsSection.vue b/src/components/FriendsSection.vue deleted file mode 100644 index 13e5a52..0000000 --- a/src/components/FriendsSection.vue +++ /dev/null @@ -1,347 +0,0 @@ - - - - - diff --git a/src/components/GithubSection.vue b/src/components/GithubSection.vue deleted file mode 100644 index 3610284..0000000 --- a/src/components/GithubSection.vue +++ /dev/null @@ -1,132 +0,0 @@ - - - - - diff --git a/src/components/HeroSection.vue b/src/components/HeroSection.vue deleted file mode 100644 index fb9c284..0000000 --- a/src/components/HeroSection.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - - - diff --git a/src/components/PageSwitcher.vue b/src/components/PageSwitcher.vue deleted file mode 100644 index bbedab5..0000000 --- a/src/components/PageSwitcher.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - - - diff --git a/src/components/ProjectsSection.vue b/src/components/ProjectsSection.vue deleted file mode 100644 index 19c2057..0000000 --- a/src/components/ProjectsSection.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - - - diff --git a/src/components/SitesSection.vue b/src/components/SitesSection.vue deleted file mode 100644 index a3ed6c6..0000000 --- a/src/components/SitesSection.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - - - diff --git a/src/components/SkillsSection.vue b/src/components/SkillsSection.vue deleted file mode 100644 index b332a20..0000000 --- a/src/components/SkillsSection.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - - - diff --git a/src/styles.css b/src/styles.css deleted file mode 100644 index b5e89d4..0000000 --- a/src/styles.css +++ /dev/null @@ -1,289 +0,0 @@ -html { - height: 100%; -} - -:root { - color-scheme: light dark; - font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - background: #0f1629; - color: #e8eefc; -} - -body { - margin: 0; - min-height: 100%; - background: radial-gradient(circle at 20% 20%, #1b2b4b, #0f1629); -} - -/* Layout shell keeps background and toggle visible even when content is hidden */ - -.app-shell { - position: relative; - min-height: 100vh; - color: #e8eefc; - overflow: hidden; - display: flex; - flex-direction: column; - isolation: isolate; -} - -.background-overlay { - position: fixed; - inset: 0; - pointer-events: none; - z-index: -1; - background-repeat: no-repeat; -} - -.content-stack { - position: relative; - z-index: 1; - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.app-body { - position: relative; - z-index: 1; - flex: 1; - display: flex; - flex-direction: column; -} - -a { - color: #7cc1ff; - text-decoration: none; -} - -.page { - max-width: 960px; - margin: 0 auto; - padding: 32px 16px 48px; - display: flex; - flex-direction: column; - gap: 16px; -} - -.card { - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 16px; - padding: 18px 20px; - backdrop-filter: blur(8px); - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25); -} - -.hero { - display: grid; - grid-template-columns: 96px 1fr; - gap: 16px; - align-items: center; -} - -.avatar { - width: 96px; - height: 96px; - border-radius: 50%; - object-fit: cover; - border: 2px solid rgba(255, 255, 255, 0.2); -} - -h1, -h2, -h3 { - margin: 0 0 8px 0; -} - -p { - margin: 6px 0; -} - -.muted { - color: #a8b3cf; - font-size: 0.95rem; -} - -.chips { - display: flex; - flex-wrap: wrap; - gap: 10px; -} - -.chips a { - padding: 6px 12px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.08); -} - -.list { - list-style: none; - padding: 0; - margin: 0; -} - -.list li { - margin-bottom: 8px; -} - -.footer { - text-align: center; - margin-top: 8px; -} - -/* Ensure the Netease mini player doesn't occupy page layout space - and stays fixed above content. Use !important to override - styles injected by third-party scripts. */ -.netease-mini-player { - position: fixed !important; - bottom: 20px !important; - left: 20px !important; - right: auto !important; - /* do not force width here — let the player's own minimized/expanded - styles control sizing. Only constrain max width as a safety net. */ - max-width: calc(100% - 40px) !important; - z-index: 10001 !important; - margin: 0 !important; - transform: none !important; -} - -/* If the player injects a full-width bar, make sure it won't - push page content by forcing it out of the normal flow. */ -.netease-mini-player > * { - box-sizing: border-box; -} - -/* Defensive: hide any accidental full-width injected container from the player - that might occupy vertical space. This targets common helper classes used - by the player script. */ -.netease-mini-player-embed, -.nmpv2-player, -.nmpv2-root { - position: fixed !important; - bottom: 20px !important; - left: 20px !important; - right: auto !important; - z-index: 10001 !important; -} - -/* Fix: prevent playlist dropdown from increasing document height - by forcing the playlist container to be fixed and clipped to viewport. */ -.netease-mini-player[data-position="bottom-left"] .playlist-container, -.netease-mini-player[data-position="bottom-right"] .playlist-container { - position: fixed !important; - bottom: calc(20px + 80px) !important; - left: 20px !important; - right: auto !important; - width: 290px !important; - max-height: 50vh !important; - overflow: auto !important; - z-index: 10002 !important; -} - -.netease-mini-player[data-position="top-left"] .playlist-container, -.netease-mini-player[data-position="top-right"] .playlist-container { - position: fixed !important; - top: calc(20px + 80px) !important; - left: 20px !important; - right: auto !important; - width: 290px !important; - max-height: 50vh !important; - overflow: auto !important; - z-index: 10002 !important; -} - -/* If player is docked to the right, align playlist to right edge */ -.netease-mini-player[data-position="bottom-right"] .playlist-container, -.netease-mini-player[data-position="top-right"] .playlist-container { - left: auto !important; - right: 20px !important; -} - -/* Ensure minimized player displays as a circle and its album cover fits */ -.netease-mini-player.minimized { - width: 80px !important; - height: 80px !important; - border-radius: 50% !important; - padding: 0 !important; - overflow: hidden !important; - box-shadow: none !important; -} - -.netease-mini-player.minimized .album-cover-container { - width: 100% !important; - height: 100% !important; - position: absolute !important; - top: 0 !important; - left: 0 !important; - border-radius: 50% !important; - overflow: hidden !important; -} - -.netease-mini-player.minimized .album-cover { - width: 100% !important; - height: 100% !important; - object-fit: cover !important; - border-radius: 50% !important; -} - -/* Floating background show/hide toggle (bottom-right) */ -.background-toggle { - position: fixed; - right: 18px; - bottom: 18px; - z-index: 10000; - display: inline-flex; - align-items: center; - gap: 6px; - padding: 10px 14px; - border-radius: 999px; - border: 1px solid rgba(255, 255, 255, 0.25); - background: rgba(255, 255, 255, 0.14); - backdrop-filter: blur(10px); - color: #f7fbff; - font-weight: 600; - cursor: pointer; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28); - transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease, border-color 0.18s ease; -} - -.background-toggle:hover, -.background-toggle:focus-visible { - background: rgba(124, 193, 255, 0.25); - border-color: rgba(124, 193, 255, 0.65); - box-shadow: 0 14px 36px rgba(124, 193, 255, 0.28); - outline: none; -} - -.background-toggle:active { - transform: translateY(1px) scale(0.99); -} - -.background-toggle .toggle-icon { - font-size: 18px; - line-height: 1; -} - -.background-toggle .toggle-label { - font-size: 14px; - letter-spacing: 0.2px; -} - -.background-toggle.active { - background: linear-gradient(135deg, rgba(124, 193, 255, 0.4), rgba(255, 255, 255, 0.2)); - border-color: rgba(124, 193, 255, 0.8); - color: #0f1629; - box-shadow: 0 16px 42px rgba(124, 193, 255, 0.32); -} - -@media (max-width: 640px) { - .background-toggle { - right: 12px; - bottom: 12px; - padding: 9px 12px; - gap: 5px; - } - .background-toggle .toggle-label { - font-size: 13px; - } -} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts deleted file mode 100644 index 68af4be..0000000 --- a/src/vite-env.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// - -declare module "*.vue" { - import type { DefineComponent } from "vue"; - const component: DefineComponent<{}, {}, any>; - export default component; -} diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..b497129 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,39 @@ +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 6b07ed8..9e2c134 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,5 +3,8 @@ "compilerOptions": { "types": ["node"] }, - "include": ["nuxt.config.ts", "src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts", "server/**/*.ts"] + "vueCompilerOptions": { + "globalTypesPath": "./node_modules/.vue-global-types" + }, + "include": ["nuxt.config.ts", "app/**/*.ts", "app/**/*.vue", "app/**/*.d.ts", "server/**/*.ts"] }