├── favicon.ico ├── public ├── robots.txt ├── manifest.json └── sitemap.xml ├── postcss.config.js ├── src ├── config │ ├── rss.ts │ ├── navigation.ts │ ├── font.ts │ ├── site-info.ts │ ├── email.ts │ ├── footer.ts │ ├── tools.ts │ ├── index.ts │ ├── site.ts │ ├── projects.ts │ └── notice.ts ├── types │ ├── blog.ts │ ├── global.d.ts │ └── notice.ts ├── main.ts ├── assets │ └── styles │ │ ├── variables.css │ │ ├── colors.css │ │ └── main.css ├── utils │ ├── font.ts │ ├── rss.ts │ ├── copyright.ts │ ├── security.ts │ └── console.ts ├── components │ ├── layout │ │ ├── ToolLayout.vue │ │ ├── TheFooter.vue │ │ └── TheHeader.vue │ ├── ui │ │ ├── Tabs.vue │ │ ├── LazyImage.vue │ │ ├── Toast.vue │ │ ├── WarningDialog.vue │ │ └── Modal.vue │ ├── ProjectCard.vue │ ├── PageTransition.vue │ ├── ThemeToggle.vue │ └── effects │ │ └── Fireworks.vue ├── views │ ├── ErrorView.vue │ ├── tools │ │ ├── JsonFormatterView.vue │ │ ├── BookmarksView.vue │ │ └── TimestampView.vue │ ├── SkillsView.vue │ ├── ToolsView.vue │ ├── BlogView.vue │ ├── ContactView.vue │ ├── HomeView.vue │ └── ProjectsView.vue ├── services │ └── rss.ts ├── router │ └── index.ts ├── env.d.ts └── App.vue ├── .vscode ├── extensions.json └── settings.json ├── .editorconfig ├── .env.development ├── tsconfig.node.json ├── tsconfig.app.json ├── .gitignore ├── api └── rss.ts ├── .env.example ├── tsconfig.json ├── tailwind.config.js ├── LICENSE ├── package.json ├── index.html ├── README.md └── vite.config.ts /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acanyo/home-for-vue/HEAD/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Sitemap: <%= config.siteUrl %>/sitemap.xml -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/config/rss.ts: -------------------------------------------------------------------------------- 1 | interface RssConfig { 2 | url: string; 3 | } 4 | 5 | export const rssConfig: RssConfig = { 6 | url: "https://www.mmm.sd/rss.xml", // 直接使用完整 URL 7 | }; 8 | -------------------------------------------------------------------------------- /src/types/blog.ts: -------------------------------------------------------------------------------- 1 | export interface BlogPost { 2 | title: string; 3 | link: string; 4 | date: Date; 5 | description: string; 6 | category?: string; 7 | image?: string; 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "vue.vscode-typescript-vue-plugin", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode", 7 | "bradlc.vscode-tailwindcss" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $toast: { 3 | show: (text: string, type?: "success" | "error" | "info") => void; 4 | success: (text: string) => void; 5 | error: (text: string) => void; 6 | info: (text: string) => void; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_SITE_URL=https://home.mmm.sd 2 | VITE_EMAILJS_SERVICE_ID=service_rwwcsdd 3 | VITE_EMAILJS_TEMPLATE_ID=template_3hwjbur 4 | VITE_EMAILJS_PUBLIC_KEY=5bLIm57heceDFPTuV 5 | VITE_GUESTBOOK_URL=https://www.mmm.sd/content 6 | VITE_RSS_URL=https://www.mmm.sd/rss.xml -------------------------------------------------------------------------------- /src/config/navigation.ts: -------------------------------------------------------------------------------- 1 | export interface Tab { 2 | id: string; 3 | label: string; 4 | icon: string; 5 | } 6 | 7 | export const tabs: Tab[] = [ 8 | { id: "projects", label: "项目展示", icon: "🎨" }, 9 | { id: "tools", label: "在线工具", icon: "🛠" }, 10 | { id: "bookmarks", label: "网址导航", icon: "🔖" }, 11 | ]; 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "types": ["vite/client"] 10 | }, 11 | "include": ["vite.config.ts", "src/config/**/*", "src/types/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import "./assets/styles/main.css"; 5 | import { initFontLoading } from "./utils/font"; 6 | 7 | const app = createApp(App); 8 | app.use(router); 9 | app.mount("#app"); 10 | 11 | // 初始化字体加载 12 | initFontLoading().then(() => { 13 | console.log("Font initialization complete"); 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": [ 4 | "env.d.ts", 5 | "src/**/*", 6 | "src/**/*.ts", 7 | "src/**/*.tsx", 8 | "src/**/*.vue" 9 | ], 10 | "exclude": ["src/**/__tests__/*"], 11 | "compilerOptions": { 12 | "composite": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["./src/*"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/config/font.ts: -------------------------------------------------------------------------------- 1 | interface FontConfig { 2 | enabled: boolean; 3 | name: string; 4 | url: string; 5 | preload?: boolean; 6 | display?: "auto" | "block" | "swap" | "fallback" | "optional"; 7 | weights?: string; 8 | } 9 | 10 | export const fontConfig: FontConfig = { 11 | enabled: true, 12 | name: "LXWK", 13 | url: "https://cdn.jsdmirror.com/gh/acanyo/mmm.sd@master/assets/font/lxwk.woff2", 14 | preload: true, 15 | display: "swap", 16 | weights: "100 900", 17 | }; 18 | -------------------------------------------------------------------------------- /src/types/notice.ts: -------------------------------------------------------------------------------- 1 | export interface NoticeButton { 2 | text: string; 3 | type?: string; 4 | action: "close" | "navigate" | "link" | "custom"; 5 | to?: string; 6 | href?: string; 7 | handler?: () => void; 8 | showAfter?: number | "refresh" | null; 9 | } 10 | 11 | export interface NoticeConfig { 12 | id: string; 13 | title: string; 14 | content: string; 15 | width?: string; 16 | maskClosable?: boolean; 17 | showClose?: boolean; 18 | buttons: NoticeButton[]; 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | !.vscode/settings.json 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | 32 | # 环境变量文件 33 | .env* 34 | !.env.example 35 | .env 36 | -------------------------------------------------------------------------------- /src/assets/styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-family-custom: "LXWK"; 3 | --font-family-system: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 4 | "Helvetica Neue", Arial, sans-serif; 5 | --font-family: var(--font-family-custom), var(--font-family-system); 6 | } 7 | 8 | /* 添加字体定义 */ 9 | @font-face { 10 | font-family: "LXWK"; 11 | font-weight: 100 900; 12 | font-display: swap; 13 | font-style: normal; 14 | src: url("https://cdn.jsdmirror.com/gh/acanyo/mmm.sd@master/assets/font/lxwk.woff2") 15 | format("woff2"); 16 | } 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Handsome | Java后端开发工程师", 3 | "short_name": "Handsome", 4 | "description": "专注于Java、Spring Boot、微服务等后端技术开发的个人作品集网站", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "background_color": "#ffffff", 8 | "theme_color": "#4F46E5", 9 | 10 | "icons": [ 11 | { 12 | "src": "https://www.mmm.sd/upload/logo.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "https://www.mmm.sd/upload/logo.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /api/rss.ts: -------------------------------------------------------------------------------- 1 | // Vercel Serverless Function 2 | export default async function handler(res) { 3 | try { 4 | const rssUrl = process.env.RSS_URL; 5 | if (!rssUrl) { 6 | throw new Error("RSS_URL environment variable is not defined"); 7 | } 8 | 9 | const response = await fetch(rssUrl); 10 | const data = await response.text(); 11 | 12 | res.setHeader("Access-Control-Allow-Origin", "*"); 13 | res.setHeader("Content-Type", "application/xml"); 14 | res.status(200).send(data); 15 | } catch (error) { 16 | res.status(500).json({ error: "Failed to fetch RSS" }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/config/site-info.ts: -------------------------------------------------------------------------------- 1 | interface SiteInfo { 2 | enabled: boolean; 3 | text: string; 4 | link: string; 5 | position?: "top" | "bottom"; 6 | theme?: "dark" | "light"; 7 | style?: string; 8 | linkStyle?: string; 9 | version?: string; 10 | } 11 | 12 | export const siteInfo: SiteInfo = { 13 | enabled: true, 14 | text: "一个使用 Vue 3 + TypeScript + Vite 构建的现代化个人主页,具有博客文章展示、项目展示、联系表单等功能。", 15 | version: "V.2.3", 16 | link: "https://github.com/acanyo/home-for-vue", 17 | position: "bottom", 18 | theme: "dark", 19 | style: "position: fixed; bottom: 0; left: 0; width: 100%; z-index: 1000;", 20 | }; 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "explorer.fileNesting.enabled": true, 3 | "explorer.fileNesting.patterns": { 4 | "tsconfig.json": "tsconfig.*.json, env.d.ts", 5 | "vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*", 6 | "package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .prettier*, prettier*, .editorconfig" 7 | }, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": "explicit" 10 | }, 11 | "editor.formatOnSave": true, 12 | "editor.defaultFormatter": "esbenp.prettier-vscode", 13 | "css.validate": false, 14 | "less.validate": false, 15 | "scss.validate": false 16 | } 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 站点配置 2 | VITE_SITE_URL=https://example.com 3 | VITE_APP_TITLE=Your Site Title 4 | VITE_APP_DESCRIPTION=Your site description here 5 | VITE_APP_KEYWORDS=keyword1,keyword2,keyword3 6 | VITE_APP_AUTHOR=Your Name 7 | VITE_APP_URL=https://example.com 8 | VITE_APP_LOGO=https://example.com/logo.png 9 | 10 | # 社交媒体配置 11 | VITE_APP_TWITTER=@your_twitter 12 | VITE_APP_TWITTER_URL=https://twitter.com/your_twitter 13 | VITE_APP_GITHUB=https://github.com/your_github 14 | 15 | # 主题配置 16 | VITE_APP_THEME_COLOR=#4F46E5 17 | 18 | # EmailJS 配置 19 | VITE_EMAILJS_SERVICE_ID=your_service_id 20 | VITE_EMAILJS_TEMPLATE_ID=your_template_id 21 | VITE_EMAILJS_PUBLIC_KEY=your_public_key -------------------------------------------------------------------------------- /src/config/email.ts: -------------------------------------------------------------------------------- 1 | import emailjs from "@emailjs/browser"; 2 | 3 | interface EmailConfig { 4 | serviceId: string; 5 | templateId: string; 6 | publicKey: string; 7 | } 8 | 9 | export const emailConfig: EmailConfig = { 10 | serviceId: import.meta.env.VITE_EMAILJS_SERVICE_ID, 11 | templateId: import.meta.env.VITE_EMAILJS_TEMPLATE_ID, 12 | publicKey: import.meta.env.VITE_EMAILJS_PUBLIC_KEY, 13 | }; 14 | 15 | export const initEmailJS = () => { 16 | emailjs.init(emailConfig.publicKey); 17 | }; 18 | 19 | if ( 20 | !emailConfig.serviceId || 21 | !emailConfig.templateId || 22 | !emailConfig.publicKey 23 | ) { 24 | throw new Error("Missing required EmailJS configuration"); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/font.ts: -------------------------------------------------------------------------------- 1 | export const initFontLoading = async () => { 2 | try { 3 | // 等待字体加载 4 | await document.fonts.load('1em "LXWK"'); 5 | 6 | // 检查字体是否加载成功 7 | const isLoaded = document.fonts.check('1em "LXWK"'); 8 | console.log("Font loaded:", isLoaded); 9 | 10 | if (!isLoaded) { 11 | // 如果字体加载失败,使用系统字体 12 | document.documentElement.style.fontFamily = 13 | '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; 14 | } 15 | } catch (error) { 16 | console.error("Font loading error:", error); 17 | // 出错时使用系统字体 18 | document.documentElement.style.fontFamily = 19 | '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@/*": ["./src/*"] 18 | }, 19 | "types": [ 20 | "node", 21 | "vite/client", 22 | "@types/node" 23 | ] 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "src/**/*.d.ts", 28 | "src/**/*.tsx", 29 | "src/**/*.vue" 30 | ], 31 | "references": [{ "path": "./tsconfig.node.json" }] 32 | } 33 | -------------------------------------------------------------------------------- /src/components/layout/ToolLayout.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= config.siteUrl %>/ 5 | 2024-03-21 6 | weekly 7 | 1.0 8 | 9 | 10 | <%= config.siteUrl %>/blog 11 | 2024-03-21 12 | weekly 13 | 0.8 14 | 15 | 16 | <%= config.siteUrl %>/skills 17 | 2024-03-21 18 | monthly 19 | 0.8 20 | 21 | 22 | <%= config.siteUrl %>/contact 23 | 2024-03-21 24 | monthly 25 | 0.7 26 | 27 | -------------------------------------------------------------------------------- /src/config/footer.ts: -------------------------------------------------------------------------------- 1 | interface FooterLink { 2 | text: string; 3 | to?: string; // 内部路由 4 | href?: string; // 外部链接 5 | target?: string; 6 | } 7 | 8 | interface FooterConfig { 9 | links: FooterLink[]; 10 | provider: { 11 | name: string; 12 | link: string; 13 | logo: string; 14 | text: string; 15 | }; 16 | } 17 | 18 | export const footerConfig: FooterConfig = { 19 | links: [ 20 | { 21 | text: "联系", 22 | to: "/contact", 23 | }, 24 | 25 | { 26 | text: "博客", 27 | href: "https://www.mmm.sd", 28 | target: "_blank", 29 | }, 30 | { 31 | text: "GitHub", 32 | href: "https://github.com/acanyo", 33 | target: "_blank", 34 | }, 35 | ], 36 | provider: { 37 | name: "雨云", 38 | link: "https://www.rainyun.com/handsome_", 39 | logo: "https://app.rainyun.com/img/icons/apple-touch-icon-152x152.png", 40 | text: "提供 CDN 加速 / 云存储服务", 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/views/ErrorView.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 36 | -------------------------------------------------------------------------------- /src/config/tools.ts: -------------------------------------------------------------------------------- 1 | import JsonFormatterView from "@/views/tools/JsonFormatterView.vue"; 2 | import TimestampView from "@/views/tools/TimestampView.vue"; 3 | 4 | export interface Tool { 5 | id: number; 6 | title: string; 7 | description: string; 8 | tags: string[]; 9 | image: string; 10 | component: any; 11 | status: "completed" | "developing" | "planning"; 12 | } 13 | 14 | export const tools: Tool[] = [ 15 | { 16 | id: 1, 17 | title: "JSON 格式化工具", 18 | description: "在线 JSON 格式化工具,支持压缩、美化、验证和转换等功能", 19 | tags: ["JSON", "格式化", "在线工具"], 20 | image: "https://picsum.photos/800/600?random=1", 21 | component: JsonFormatterView, 22 | status: "completed", 23 | }, 24 | { 25 | id: 2, 26 | title: "时间戳转换器", 27 | description: "时间戳与日期格式互转工具,支持多种格式和时区设置", 28 | tags: ["时间戳", "日期转换", "时区"], 29 | image: "https://picsum.photos/800/600?random=2", 30 | component: TimestampView, 31 | status: "completed", 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /src/components/ui/Tabs.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 45 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import { siteConfig } from "./site"; 2 | 3 | // 导出所有配置 4 | export { siteConfig }; 5 | 6 | // 合并基础配置 7 | export const config = { 8 | ...siteConfig, 9 | siteUrl: "https://home.mmm.sd", // 默认值 10 | }; 11 | 12 | // 生成 sitemap.xml 内容 13 | export const generateSitemap = ( 14 | siteUrl: string, 15 | ) => ` 16 | 17 | 18 | ${siteUrl}/ 19 | 2024-03-21 20 | weekly 21 | 1.0 22 | 23 | 24 | ${siteUrl}/blog 25 | 2024-03-21 26 | weekly 27 | 0.8 28 | 29 | 30 | ${siteUrl}/skills 31 | 2024-03-21 32 | monthly 33 | 0.8 34 | 35 | 36 | ${siteUrl}/contact 37 | 2024-03-21 38 | monthly 39 | 0.7 40 | 41 | `; 42 | 43 | // 生成 robots.txt 内容 44 | export const generateRobots = (siteUrl: string) => `User-agent: * 45 | Allow: / 46 | Sitemap: ${siteUrl}/sitemap.xml`; 47 | -------------------------------------------------------------------------------- /src/config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | // 基本信息 3 | name: "Handsome", // 作者名称 4 | title: "Java后端开发工程师", // 职位头衔 5 | siteName: "Handsome | Java后端开发工程师", // 网站标题 6 | siteDescription: 7 | "专注于Java、Spring Boot、微服务等后端技术开发的个人作品集网站", // 网站描述 8 | siteKeywords: "Java开发,Spring Boot,微服务,后端开发,个人作品集,技术博客", // SEO关键词 9 | author: "Handsome", // 作者信息 10 | 11 | // 图片资源配置 12 | images: { 13 | logo: "https://www.mmm.sd/upload/logo.png", // 网站Logo 14 | icon: "https://www.mmm.sd/upload/logo.png", // 网站图标 15 | avatar: "https://www.mmm.sd/upload/logo.png", // 个人头像 16 | ogImage: "https://www.mmm.sd/upload/logo.png", // 社交分享图片 17 | }, 18 | 19 | // 个性化配置 20 | slogan: "生活原本沉闷,但跑起来就有风。", // 个性签名 21 | skills: ["Java", "Spring Boot", "MySQL", "Redis", "Docker", "Git"], // 技能标签 22 | 23 | // SEO 相关配置 24 | language: "zh-CN", // 网站语言 25 | themeColor: "#4F46E5", // 主题色 26 | twitterHandle: "@hanmdsomeX", // Twitter账号 27 | githubHandle: "acanyo", // GitHub账号 28 | 29 | // Schema.org 结构化数据 30 | organization: { 31 | name: "Handsome's Portfolio", // 组织名称 32 | logo: "https://www.mmm.sd/upload/logo.png", // 组织Logo 33 | }, 34 | 35 | // 社交媒体链接 36 | social: { 37 | github: "https://github.com/acanyo", // GitHub主页 38 | email: "30819792@qq.com", // 联系邮箱 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/rss.ts: -------------------------------------------------------------------------------- 1 | import type { BlogPost } from "../types/blog"; 2 | import { rssConfig } from "@/config/rss"; 3 | 4 | // 使用 RSS2JSON API 转换 RSS 为 JSON 5 | export const fetchBlogPosts = async (): Promise => { 6 | try { 7 | if (!rssConfig.url) { 8 | throw new Error("RSS URL is not defined"); 9 | } 10 | 11 | // 使用 RSS2JSON 服务 12 | const apiUrl = `https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(rssConfig.url)}`; 13 | const response = await fetch(apiUrl, { 14 | headers: { 15 | "Cache-Control": "no-cache", 16 | "Pragma": "no-cache" 17 | } 18 | }); 19 | 20 | if (!response.ok) { 21 | throw new Error(`HTTP error! status: ${response.status}`); 22 | } 23 | 24 | const data = await response.json(); 25 | 26 | if (data.status !== "ok") { 27 | throw new Error("Failed to fetch RSS feed"); 28 | } 29 | 30 | return data.items.map( 31 | (item: any): BlogPost => ({ 32 | title: item.title, 33 | link: item.link, 34 | date: new Date(item.pubDate), 35 | description: item.content || item.description, 36 | category: item.category || "默认分类", 37 | }), 38 | ); 39 | } catch (error) { 40 | console.error("获取博客文章失败:", error); 41 | return []; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { fontFamily } from "tailwindcss/defaultTheme"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | darkMode: "class", 6 | content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], 7 | theme: { 8 | extend: { 9 | colors: { 10 | primary: { 11 | DEFAULT: "var(--color-primary)", 12 | dark: "var(--color-primary-dark)", 13 | light: "var(--color-primary-light)", 14 | 10: "var(--color-primary-10)", 15 | }, 16 | }, 17 | backgroundColor: { 18 | main: "var(--color-bg-main)", 19 | secondary: "var(--color-bg-secondary)", 20 | tertiary: "var(--color-bg-tertiary)", 21 | }, 22 | textColor: { 23 | primary: "var(--color-text-primary)", 24 | secondary: "var(--color-text-secondary)", 25 | tertiary: "var(--color-text-tertiary)", 26 | }, 27 | borderColor: { 28 | DEFAULT: "var(--color-border)", 29 | light: "var(--color-border-light)", 30 | }, 31 | boxShadow: { 32 | sm: "var(--shadow-sm)", 33 | DEFAULT: "var(--shadow)", 34 | md: "var(--shadow-md)", 35 | }, 36 | fontFamily: { 37 | sans: ["Inter var", ...fontFamily.sans], 38 | }, 39 | }, 40 | }, 41 | plugins: [], 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/ui/LazyImage.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Handsome 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | Additional Terms: 24 | 25 | 1. The footer copyright notice and author attribution must be preserved. 26 | 2. Any modifications to the footer must maintain the original author's credit. 27 | 3. Commercial use requires explicit permission from the author. 28 | -------------------------------------------------------------------------------- /src/config/projects.ts: -------------------------------------------------------------------------------- 1 | export interface Project { 2 | id: number; 3 | title: string; 4 | description: string; 5 | tags: string[]; 6 | image: string; 7 | link?: string; 8 | status: "completed" | "developing" | "planning"; 9 | } 10 | 11 | export const projects: Project[] = [ 12 | { 13 | id: 2, 14 | title: "Handsome's Blog", 15 | description: 16 | "心若有所向往,何惧道阻且长。记录技术分享和生活感悟的个人博客。", 17 | tags: ["技术分享", "Blog", "Markdown"], 18 | image: "https://picsum.photos/800/600?random=3", 19 | link: "https://www.mmm.sd", 20 | status: "completed", 21 | }, 22 | { 23 | id: 3, 24 | title: "Status", 25 | description: "服务状态监控页面,实时监控各项服务的运行状态。", 26 | tags: ["监控", "服务状态", "实时数据"], 27 | image: "https://picsum.photos/800/600?random=4", 28 | link: "https://status.mmm.sd", 29 | status: "completed", 30 | }, 31 | { 32 | id: 4, 33 | title: "Umami Analytics", 34 | description: 35 | "开源、隐私友好的网站访问统计分析工具,提供详细的访问数据和洞察。", 36 | tags: ["数据分析", "统计", "开源"], 37 | image: "https://picsum.photos/800/600?random=5", 38 | link: "https://umami.tvtvz.cn", 39 | status: "completed", 40 | }, 41 | { 42 | id: 5, 43 | title: "AList", 44 | description: "支持多种存储的文件列表程序,提供统一的文件管理和访问接口。", 45 | tags: ["文件管理", "存储", "云服务"], 46 | image: "https://picsum.photos/800/600?random=6", 47 | link: "https://alist.mmm.sd", 48 | status: "completed", 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /src/assets/styles/colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* 主色调 */ 3 | --color-primary: #3b82f6; 4 | --color-primary-dark: #2563eb; 5 | --color-primary-light: #60a5fa; 6 | --color-primary-10: rgba(59, 130, 246, 0.1); 7 | 8 | /* 背景色 */ 9 | --color-bg-main: #ffffff; 10 | --color-bg-secondary: #f9fafb; 11 | --color-bg-tertiary: #f3f4f6; 12 | 13 | /* 文本颜色 */ 14 | --color-text-primary: #111827; 15 | --color-text-secondary: #4b5563; 16 | --color-text-tertiary: #6b7280; 17 | 18 | /* 边框颜色 */ 19 | --color-border: #e5e7eb; 20 | --color-border-light: #f3f4f6; 21 | 22 | /* 卡片阴影 */ 23 | --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); 24 | --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 25 | --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 26 | } 27 | 28 | /* 深色模式 */ 29 | :root[class~="dark"] { 30 | /* 背景色 */ 31 | --color-bg-main: #111827; 32 | --color-bg-secondary: #1f2937; 33 | --color-bg-tertiary: #374151; 34 | 35 | /* 文本颜色 */ 36 | --color-text-primary: #f9fafb; 37 | --color-text-secondary: #e5e7eb; 38 | --color-text-tertiary: #d1d5db; 39 | 40 | /* 边框颜色 */ 41 | --color-border: #374151; 42 | --color-border-light: #1f2937; 43 | 44 | /* 卡片阴影 */ 45 | --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); 46 | --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3); 47 | --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3); 48 | } 49 | -------------------------------------------------------------------------------- /src/config/notice.ts: -------------------------------------------------------------------------------- 1 | import type { NoticeButton, NoticeConfig } from "../types/notice"; 2 | 3 | interface ExtendedNoticeButton extends NoticeButton { 4 | type: "primary" | "secondary" | "danger"; 5 | } 6 | 7 | interface ExtendedNoticeConfig extends NoticeConfig { 8 | enabled: boolean; 9 | showFireworks: boolean; 10 | defaultShowAfter?: number | "refresh" | null; 11 | buttons: ExtendedNoticeButton[]; 12 | } 13 | 14 | export const noticeConfig: ExtendedNoticeConfig = { 15 | id: "site_notice_v1", 16 | enabled: true, 17 | showFireworks: true, 18 | title: "网站公告", 19 | content: ` 20 |
21 |

22 | 🎉 网站改版升级公告 23 |

24 |
25 |

网站已完成改版升级,新增以下功能:

26 |
    27 |
  • 全新的深色模式支持
  • 28 |
  • 性能优化与体验提升
  • 29 |
  • 更多实用工具正在开发中
  • 30 |
31 |
32 |
33 | `, 34 | width: "500px", 35 | maskClosable: true, 36 | showClose: true, 37 | defaultShowAfter: null, 38 | buttons: [ 39 | { 40 | text: "稍后查看", 41 | type: "secondary", 42 | action: "close", 43 | showAfter: "refresh", 44 | }, 45 | { 46 | text: "立即体验", 47 | type: "primary", 48 | action: "navigate", 49 | to: "/projects", 50 | showAfter: 3 * 60 * 60 * 1000, 51 | }, 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/ProjectCard.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 51 | 52 | 61 | -------------------------------------------------------------------------------- /src/services/rss.ts: -------------------------------------------------------------------------------- 1 | export interface BlogPost { 2 | title: string; 3 | link: string; 4 | content: string; 5 | creator: string; 6 | pubDate: string; 7 | categories?: string[]; 8 | description?: string; 9 | } 10 | 11 | export async function fetchBlogPosts(rssUrl: string): Promise { 12 | try { 13 | const response = await fetch(rssUrl, { 14 | headers: { 15 | Accept: "application/xml, text/xml, */*", 16 | }, 17 | }); 18 | const xmlText = await response.text(); 19 | const parser = new DOMParser(); 20 | const xmlDoc = parser.parseFromString(xmlText, "text/xml"); 21 | 22 | const items = xmlDoc.querySelectorAll("item"); 23 | 24 | return Array.from(items).map((item) => { 25 | const getElementText = (tagName: string) => 26 | item.querySelector(tagName)?.textContent?.trim() || ""; 27 | 28 | const getCleanContent = (content: string) => { 29 | return content.replace("", ""); 30 | }; 31 | 32 | const description = getElementText("description"); 33 | const content = description.includes("CDATA") 34 | ? getCleanContent(description) 35 | : description; 36 | 37 | return { 38 | title: getCleanContent(getElementText("title")), 39 | link: getElementText("link"), 40 | content: content, 41 | creator: "Handsome", 42 | pubDate: getElementText("pubDate"), 43 | categories: [getElementText("category")].filter(Boolean), 44 | description: content, 45 | }; 46 | }); 47 | } catch (error) { 48 | console.error("获取博客文章失败:", error); 49 | return []; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/PageTransition.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 87 | -------------------------------------------------------------------------------- /src/assets/styles/main.css: -------------------------------------------------------------------------------- 1 | @import "./colors.css"; 2 | @import "./variables.css"; 3 | @import "tailwindcss/base"; 4 | @import "tailwindcss/components"; 5 | @import "tailwindcss/utilities"; 6 | 7 | /* 添加字体定义 */ 8 | @font-face { 9 | font-family: "LXWK"; 10 | font-weight: 100 900; 11 | font-display: swap; 12 | font-style: normal; 13 | src: url("https://cdn.jsdmirror.com/gh/acanyo/mmm.sd@master/assets/font/lxwk.woff2") 14 | format("woff2"); 15 | } 16 | 17 | @layer base { 18 | html { 19 | font-family: 20 | "LXWK", 21 | -apple-system, 22 | BlinkMacSystemFont, 23 | "Segoe UI", 24 | Roboto, 25 | "Helvetica Neue", 26 | Arial, 27 | sans-serif; 28 | scroll-behavior: smooth; 29 | -webkit-tap-highlight-color: transparent; 30 | } 31 | 32 | body { 33 | @apply bg-main text-primary antialiased; 34 | } 35 | 36 | /* 移动端优化 */ 37 | @media (max-width: 768px) { 38 | html { 39 | font-size: 14px; 40 | } 41 | } 42 | 43 | /* 移动端点击态优化 */ 44 | @media (hover: none) { 45 | .hover\:scale-105:active { 46 | transform: scale(1.02); 47 | } 48 | } 49 | } 50 | 51 | @layer components { 52 | .btn-primary { 53 | @apply inline-block px-6 py-3 bg-primary text-white rounded-lg 54 | hover:bg-primary-dark transition-colors duration-300; 55 | } 56 | 57 | .btn-secondary { 58 | @apply inline-block px-6 py-3 border-2 border-primary text-primary rounded-lg 59 | hover:bg-primary hover:text-white transition-colors duration-300; 60 | } 61 | 62 | .card { 63 | @apply bg-main border border-light rounded-2xl shadow-sm 64 | hover:shadow-md transition-all duration-300; 65 | } 66 | } 67 | 68 | /* 移动端滚动优化 */ 69 | .smooth-scroll { 70 | -webkit-overflow-scrolling: touch; 71 | scroll-behavior: smooth; 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/copyright.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2024 Handsome 4 | * 5 | * This file is part of the project and must retain the author's credit. 6 | * Modifications to this file must maintain original attribution. 7 | * Commercial use requires explicit permission. 8 | */ 9 | 10 | // 使用一个自执行函数来增加混淆难度 11 | export const createCopyrightGuard = (() => { 12 | const key = btoa("Handsome" + new Date().getFullYear()); 13 | const targetUrl = atob("aHR0cHM6Ly93d3cubW1tLnNkLw=="); // 加密的跳转URL 14 | 15 | return (element: HTMLElement | null) => { 16 | if (!element) return false; 17 | 18 | // 随机检查函数 19 | const checks = [ 20 | () => element.textContent?.includes("©"), 21 | () => element.textContent?.includes("Handsome"), 22 | () => element.textContent?.includes("All rights"), 23 | () => element.querySelector("a")?.href.includes("mmm.sd"), 24 | () => !element.textContent?.includes("Modified"), 25 | () => element.children.length >= 3, 26 | ]; 27 | 28 | // 随机打乱检查顺序 29 | const shuffledChecks = checks.sort(() => Math.random() - 0.5); 30 | 31 | // 执行所有检查 32 | const isValid = shuffledChecks.every((check) => { 33 | try { 34 | return check(); 35 | } catch { 36 | return false; 37 | } 38 | }); 39 | 40 | if (!isValid) { 41 | // 使用多种方式跳转,增加绕过难度 42 | try { 43 | const methods = [ 44 | () => window.location.replace(targetUrl), 45 | () => (window.location.href = targetUrl), 46 | () => window.location.assign(targetUrl), 47 | () => window.open(targetUrl, "_self"), 48 | ]; 49 | methods[Math.floor(Math.random() * methods.length)](); 50 | } catch { 51 | window.location.href = targetUrl; 52 | } 53 | } 54 | 55 | return isValid; 56 | }; 57 | })(); 58 | -------------------------------------------------------------------------------- /src/components/ui/Toast.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "home-for-vue", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "author": { 6 | "name": "Handsome", 7 | "url": "https://www.mmm.sd/" 8 | }, 9 | "private": true, 10 | "type": "module", 11 | "scripts": { 12 | "dev": "vite", 13 | "build": "run-p type-check \"build-only {@}\" --", 14 | "build-only": "vite build", 15 | "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", 16 | "preview": "vite preview", 17 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 18 | "format": "prettier --write src/" 19 | }, 20 | "dependencies": { 21 | "@emailjs/browser": "^4.4.1", 22 | "pinia": "^2.1.7", 23 | "rss-parser": "^3.13.0", 24 | "vue": "^3.4.3", 25 | "vue-router": "^4.2.5" 26 | }, 27 | "devDependencies": { 28 | "@rushstack/eslint-patch": "^1.3.3", 29 | "@tsconfig/node18": "^18.2.2", 30 | "@types/node": "^20.17.10", 31 | "@vitejs/plugin-vue": "^4.5.2", 32 | "@vue/eslint-config-prettier": "^8.0.0", 33 | "@vue/eslint-config-typescript": "^12.0.0", 34 | "@vue/tsconfig": "^0.5.0", 35 | "autoprefixer": "^10.4.16", 36 | "eslint": "^8.49.0", 37 | "eslint-plugin-vue": "^9.17.0", 38 | "imagemin": "^9.0.0", 39 | "imagemin-gifsicle": "^7.0.0", 40 | "imagemin-mozjpeg": "^10.0.0", 41 | "imagemin-optipng": "^8.0.0", 42 | "imagemin-pngquant": "^10.0.0", 43 | "imagemin-svgo": "^11.0.1", 44 | "npm-run-all": "^4.1.5", 45 | "postcss": "^8.4.32", 46 | "prettier": "^3.0.3", 47 | "sharp": "^0.33.5", 48 | "tailwindcss": "^3.4.0", 49 | "terser": "^5.37.0", 50 | "typescript": "~5.3.0", 51 | "vite": "^5.0.0", 52 | "vite-plugin-compression": "^0.5.1", 53 | "vite-plugin-image-optimizer": "^1.1.8", 54 | "vite-plugin-imagemin": "^0.6.1", 55 | "vue-tsc": "^1.8.25" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/security.ts: -------------------------------------------------------------------------------- 1 | // 禁用开发者工具和快捷键 2 | export const initSecurityMeasures = () => { 3 | let warningShown = false; 4 | let warningTimeout: number | null = null; 5 | 6 | // 显示友好提示 7 | const showWarning = () => { 8 | if (!warningShown) { 9 | warningShown = true; 10 | const warningDialog = document.querySelector("#warning-dialog"); 11 | if (warningDialog) { 12 | warningDialog.open(); 13 | // 清除之前的定时器 14 | if (warningTimeout) { 15 | clearTimeout(warningTimeout); 16 | } 17 | // 3秒后自动关闭 18 | warningTimeout = window.setTimeout(() => { 19 | warningDialog.close(); 20 | warningShown = false; 21 | warningTimeout = null; 22 | }, 3000); 23 | } 24 | } 25 | }; 26 | 27 | // 监听右键菜单 28 | document.addEventListener( 29 | "contextmenu", 30 | (e: MouseEvent) => { 31 | e.preventDefault(); 32 | showWarning(); 33 | }, 34 | true, 35 | ); 36 | 37 | // 监听开发者工具快捷键 38 | document.addEventListener( 39 | "keydown", 40 | (e: KeyboardEvent) => { 41 | const isMacCmd = e.metaKey && !e.ctrlKey; 42 | const isWinCtrl = e.ctrlKey && !e.metaKey; 43 | 44 | if ( 45 | e.key === "F12" || 46 | ((isMacCmd || isWinCtrl) && 47 | e.shiftKey && 48 | ["I", "J", "C", "U"].includes(e.key.toUpperCase())) 49 | ) { 50 | e.preventDefault(); 51 | showWarning(); 52 | } 53 | }, 54 | true, 55 | ); 56 | 57 | // 检测开发者工具状态 58 | const checkDevTools = () => { 59 | const threshold = 160; 60 | const widthThreshold = window.outerWidth - window.innerWidth > threshold; 61 | const heightThreshold = window.outerHeight - window.innerHeight > threshold; 62 | 63 | if (widthThreshold || heightThreshold) { 64 | showWarning(); 65 | } 66 | }; 67 | 68 | // 每 1000ms 检查一次 69 | setInterval(checkDevTools, 1000); 70 | }; 71 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %VITE_APP_TITLE% 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 44 | 45 | 46 | 53 | 54 | 55 |
56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/utils/console.ts: -------------------------------------------------------------------------------- 1 | interface ConsoleInfo { 2 | text: string; 3 | version: string | undefined; 4 | link: string; 5 | style?: string; 6 | } 7 | 8 | export const printConsoleInfo = (info: ConsoleInfo) => { 9 | // 标题样式 10 | const titleStyle = [ 11 | "background: linear-gradient(45deg, #2193b0, #6dd5ed)", 12 | "color: white", 13 | "padding: 12px 20px", 14 | "border-radius: 4px 0 0 4px", 15 | "font-weight: bold", 16 | "font-size: 13px", 17 | "text-shadow: 0 1px 1px rgba(0,0,0,0.2)", 18 | "box-shadow: inset 0 -3px 0 rgba(0,0,0,0.1)", 19 | ].join(";"); 20 | 21 | // 版本样式 22 | const versionStyle = [ 23 | "background: linear-gradient(45deg, #6dd5ed, #2193b0)", 24 | "color: white", 25 | "padding: 12px 20px", 26 | "font-weight: bold", 27 | "font-size: 13px", 28 | "text-shadow: 0 1px 1px rgba(0,0,0,0.2)", 29 | "box-shadow: inset 0 -3px 0 rgba(0,0,0,0.1)", 30 | ].join(";"); 31 | 32 | // 链接样式 33 | const linkStyle = [ 34 | "background: linear-gradient(45deg, #2193b0, #6dd5ed)", 35 | "color: white", 36 | "padding: 12px 20px", 37 | "border-radius: 0 4px 4px 0", 38 | "font-weight: bold", 39 | "font-size: 13px", 40 | "text-shadow: 0 1px 1px rgba(0,0,0,0.2)", 41 | "box-shadow: inset 0 -3px 0 rgba(0,0,0,0.1)", 42 | ].join(";"); 43 | 44 | // 主信息 45 | console.log( 46 | `%c ${info.text} %c ${info.version || ""} %c ${info.link} `, 47 | titleStyle, 48 | versionStyle, 49 | linkStyle, 50 | ); 51 | 52 | // 欢迎信息 53 | const welcomeStyle = [ 54 | "color: #2193b0", 55 | "font-size: 14px", 56 | "font-weight: bold", 57 | "padding: 12px 20px", 58 | "margin: 20px 0", 59 | "border: 2px solid #2193b0", 60 | "border-radius: 4px", 61 | "background: rgba(33,147,176,0.1)", 62 | "text-shadow: 0 1px 1px rgba(255,255,255,0.8)", 63 | ].join(";"); 64 | 65 | console.log("%c欢迎访问我的个人主页!", welcomeStyle); 66 | 67 | // 装饰线 68 | const lineStyle = [ 69 | "font-size: 1px", 70 | "padding: 0", 71 | "margin: 4px 0", 72 | "line-height: 1px", 73 | "background: linear-gradient(to right, #2193b0, #6dd5ed)", 74 | ].join(";"); 75 | 76 | console.log("%c ", `${lineStyle}; padding: 2px 125px;`); 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/ui/WarningDialog.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 71 | 72 | 90 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router"; 2 | import type { RouteRecordRaw } from "vue-router"; 3 | import HomeView from "@/views/HomeView.vue"; 4 | 5 | const routes: RouteRecordRaw[] = [ 6 | { 7 | path: "/", 8 | name: "home", 9 | component: HomeView, 10 | meta: { 11 | title: "个人作品集 | 首页", 12 | }, 13 | }, 14 | { 15 | path: "/skills", 16 | name: "skills", 17 | component: () => import("@/views/SkillsView.vue"), 18 | meta: { 19 | title: "技能", 20 | transition: "slide-right", 21 | }, 22 | }, 23 | { 24 | path: "/projects", 25 | name: "projects", 26 | component: () => import("@/views/ProjectsView.vue"), 27 | meta: { 28 | title: "项目", 29 | transition: "slide-left", 30 | }, 31 | }, 32 | { 33 | path: "/tools", 34 | name: "tools", 35 | component: () => import("@/views/ToolsView.vue"), 36 | meta: { 37 | title: "工具箱", 38 | transition: "fade", 39 | }, 40 | children: [ 41 | { 42 | path: "json", 43 | name: "json-formatter", 44 | component: () => import("@/views/tools/JsonFormatterView.vue"), 45 | meta: { 46 | title: "JSON 格式化", 47 | transition: "fade", 48 | }, 49 | }, 50 | { 51 | path: "bookmarks", 52 | name: "bookmarks", 53 | component: () => import("@/views/tools/BookmarksView.vue"), 54 | meta: { 55 | title: "网址导航", 56 | transition: "fade", 57 | }, 58 | }, 59 | { 60 | path: "timestamp", 61 | name: "timestamp", 62 | component: () => import("@/views/tools/TimestampView.vue"), 63 | meta: { 64 | title: "时间戳转换", 65 | transition: "fade", 66 | }, 67 | }, 68 | ], 69 | }, 70 | { 71 | path: "/blog", 72 | name: "blog", 73 | component: () => import("@/views/BlogView.vue"), 74 | meta: { 75 | title: "技术博客", 76 | }, 77 | }, 78 | { 79 | path: "/contact", 80 | name: "contact", 81 | component: () => import("@/views/ContactView.vue"), 82 | meta: { 83 | title: "联系", 84 | transition: "scale", 85 | }, 86 | }, 87 | ]; 88 | 89 | const router = createRouter({ 90 | history: createWebHistory(import.meta.env.BASE_URL), 91 | routes, 92 | scrollBehavior(to, from, savedPosition) { 93 | if (savedPosition) { 94 | return savedPosition; 95 | } 96 | return { top: 0 }; 97 | }, 98 | }); 99 | 100 | // 路由标题 101 | router.beforeEach((to, from, next) => { 102 | document.title = `${to.meta.title || "首页"} | Handsome`; 103 | next(); 104 | }); 105 | 106 | export default router; 107 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 70 | 71 | 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Home For Vue 2 | 3 | 一个使用 Vue 3 + TypeScript + Vite 构建的现代化个人主页,具有博客文章展示、项目展示、联系表单等功能。 4 | 5 | # 💬交流 6 | ![群.png](https://www.lik.cc/upload/iShot_2025-03-03_16.03.00.png) 7 | 8 | 9 | ## 特性 10 | 11 | - 🚀 使用 Vue 3 + TypeScript + Vite 构建 12 | - 🎨 支持深色模式 13 | - 📱 响应式设计,支持移动端 14 | - ⚡️ 快速加载和页面切换 15 | - 🔍 SEO 友好 16 | - 🌐 支持多语言 17 | - 📝 Markdown 博客支持 18 | - 📦 组件自动导入 19 | - 🎯 TypeScript 类型安全 20 | - 🔧 可配置的主题 21 | 22 | ## 技术栈 23 | 24 | - Vue 3 25 | - TypeScript 26 | - Vite 27 | - Vue Router 28 | - TailwindCSS 29 | - PostCSS 30 | - ESLint + Prettier 31 | - Husky + lint-staged 32 | 33 | ## 开发 34 | 35 | ```bash 36 | # 克隆项目 37 | git clone https://github.com/acanyo/home-for-vue.git 38 | 39 | # 安装依赖 40 | pnpm install 41 | 42 | # 启动开发服务器 43 | pnpm dev 44 | 45 | # 构建生产版本 46 | pnpm build 47 | 48 | # 预览生产构建 49 | pnpm preview 50 | 51 | # 代码格式化 52 | pnpm format 53 | 54 | # 代码检查 55 | pnpm lint 56 | ``` 57 | 58 | ## 项目结构 59 | 60 | ``` 61 | ├── public/ # 静态资源 62 | ├── src/ 63 | │ ├── assets/ # 项目资源 64 | │ ├── components/ # 组件 65 | │ ├── config/ # 配置文件 66 | │ ├── layouts/ # 布局组件 67 | │ ├── pages/ # 页面 68 | │ ├── router/ # 路由配置 69 | │ ├── styles/ # 样式文件 70 | │ ├── types/ # TypeScript 类型 71 | │ ├── utils/ # 工具函数 72 | │ ├── App.vue # 根组件 73 | │ └── main.ts # 入口文件 74 | ├── .env # 环境变量 75 | ├── index.html # HTML 模板 76 | ├── package.json # 项目配置 77 | ├── tsconfig.json # TypeScript 配置 78 | ├── vite.config.ts # Vite 配置 79 | └── README.md # 项目说明 80 | ``` 81 | 82 | ## 配置 83 | 84 | ### 站点配置 85 | 86 | 在 `src/config/site.ts` 中配置站点基本信息: 87 | 88 | ```typescript 89 | export const siteConfig = { 90 | name: "Your Site Name", 91 | description: "Your site description", 92 | // ...其他配置 93 | }; 94 | ``` 95 | 96 | ### 主题配置 97 | 98 | 在 `src/config/theme.ts` 中配置主题相关选项: 99 | 100 | ```typescript 101 | export const themeConfig = { 102 | colors: { 103 | primary: "#2196f3", 104 | // ...其他颜色 105 | }, 106 | // ...其他主题配置 107 | }; 108 | ``` 109 | 110 | ## 部署 111 | 112 | 项目可以部署到任何静态网站托管服务: 113 | 114 | ```bash 115 | # 构建项目 116 | pnpm build 117 | 118 | # 部署 dist 目录 119 | ``` 120 | 121 | ## 许可证 122 | 123 | [MIT](./LICENSE) 124 | 125 | ## 版权声明 126 | 127 | - 代码版权归作者 [Handsome](https://www.mmm.sd/) 所有 128 | - 页脚版权信息不得移除或修改 129 | - 违反协议的使用行为将被追究法律责任 130 | 131 | ### 补充条款 132 | 133 | 在遵循 MIT 许可证的基础上,还需遵守以下条款: 134 | 135 | 1. 必须保留页脚版权信息和作者署名 136 | 2. 不得修改页脚中的作者信息 137 | 3. 商业使用需获得作者明确授权 138 | 139 | ## 作者 140 | 141 | [Handsome](https://www.mmm.sd/) 142 | 143 | ## 贡献 144 | 145 | 欢迎提交 Issue 和 Pull Request! 146 | 147 | ## 推荐服务商 148 | 149 | 150 | Rainyun Logo 151 | Rainyun 152 | 153 | 提供 CDN 加速 / 云存储服务 154 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import path from "path"; 4 | import viteCompression from "vite-plugin-compression"; 5 | import { ViteImageOptimizer } from "vite-plugin-image-optimizer"; 6 | import { fontConfig } from "./src/config/font"; 7 | 8 | export default defineConfig({ 9 | base: "/", 10 | build: { 11 | outDir: "dist", 12 | assetsDir: "assets", 13 | minify: "terser", 14 | sourcemap: false, 15 | chunkSizeWarningLimit: 1500, 16 | terserOptions: { 17 | compress: { 18 | drop_console: true, 19 | drop_debugger: true, 20 | pure_funcs: ["console.log"], 21 | }, 22 | format: { 23 | comments: /@license/i, 24 | }, 25 | }, 26 | rollupOptions: { 27 | output: { 28 | manualChunks: { 29 | vendor: ["vue", "vue-router"], 30 | }, 31 | chunkFileNames: "assets/js/[name]-[hash].js", 32 | entryFileNames: "assets/js/[name]-[hash].js", 33 | assetFileNames: "assets/[ext]/[name]-[hash].[ext]", 34 | }, 35 | }, 36 | cssCodeSplit: true, 37 | cssMinify: true, 38 | }, 39 | plugins: [ 40 | vue(), 41 | viteCompression({ 42 | verbose: true, 43 | disable: false, 44 | threshold: 10240, 45 | algorithm: "gzip", 46 | ext: ".gz", 47 | }), 48 | ViteImageOptimizer({ 49 | test: /\.(jpe?g|png|gif|svg)$/i, 50 | exclude: undefined, 51 | include: undefined, 52 | includePublic: true, 53 | logStats: true, 54 | ansiColors: true, 55 | svg: { 56 | multipass: true, 57 | plugins: [ 58 | { 59 | name: "preset-default", 60 | params: { 61 | overrides: { 62 | removeViewBox: false, 63 | removeEmptyAttrs: false, 64 | }, 65 | }, 66 | }, 67 | ], 68 | }, 69 | png: { 70 | quality: 80, 71 | }, 72 | jpeg: { 73 | quality: 80, 74 | }, 75 | jpg: { 76 | quality: 80, 77 | }, 78 | tiff: { 79 | quality: 80, 80 | }, 81 | gif: undefined, 82 | webp: { 83 | quality: 80, 84 | }, 85 | avif: { 86 | quality: 80, 87 | }, 88 | }), 89 | ], 90 | resolve: { 91 | alias: { 92 | "@": path.resolve(__dirname, "src"), 93 | }, 94 | }, 95 | server: { 96 | proxy: { 97 | "/rss.xml": { 98 | target: "https://www.mmm.sd", 99 | changeOrigin: true, 100 | rewrite: (path) => path + '?t=' + Date.now(), 101 | headers: { 102 | Accept: "application/xml, text/xml, */*", 103 | "User-Agent": "Mozilla/5.0", 104 | "Cache-Control": "no-cache", 105 | "Pragma": "no-cache" 106 | }, 107 | }, 108 | }, 109 | }, 110 | define: { 111 | __VUE_OPTIONS_API__: true, 112 | __VUE_PROD_DEVTOOLS__: false, 113 | "process.env.VITE_FONT_URL": JSON.stringify(fontConfig.url), 114 | "process.env.VITE_FONT_ENABLED": JSON.stringify(fontConfig.enabled), 115 | "process.env.VITE_FONT_PRELOAD": JSON.stringify(fontConfig.preload), 116 | }, 117 | }); 118 | -------------------------------------------------------------------------------- /src/components/effects/Fireworks.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 160 | -------------------------------------------------------------------------------- /src/views/tools/JsonFormatterView.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 127 | -------------------------------------------------------------------------------- /src/components/ui/Modal.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 133 | 134 | 150 | -------------------------------------------------------------------------------- /src/components/layout/TheFooter.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 132 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare global { 4 | interface ImportMetaEnv { 5 | readonly VITE_APP_TITLE: string; 6 | readonly VITE_APP_DESCRIPTION: string; 7 | readonly VITE_APP_KEYWORDS: string; 8 | readonly VITE_APP_AUTHOR: string; 9 | readonly VITE_APP_URL: string; 10 | readonly VITE_APP_LOGO: string; 11 | readonly VITE_APP_GITHUB: string; 12 | readonly VITE_APP_TWITTER: string; 13 | readonly VITE_APP_TWITTER_URL: string; 14 | readonly VITE_APP_THEME_COLOR: string; 15 | readonly VITE_EMAILJS_SERVICE_ID: string; 16 | readonly VITE_EMAILJS_TEMPLATE_ID: string; 17 | readonly VITE_EMAILJS_PUBLIC_KEY: string; 18 | readonly VITE_SITE_URL: string; 19 | readonly DEV: boolean; 20 | readonly PROD: boolean; 21 | readonly MODE: string; 22 | } 23 | 24 | interface ImportMeta { 25 | readonly env: ImportMetaEnv; 26 | readonly hot?: { 27 | readonly data: any; 28 | accept(): void; 29 | accept(cb: (mod: any) => void): void; 30 | accept(dep: string, cb: (mod: any) => void): void; 31 | accept(deps: string[], cb: (mods: any[]) => void): void; 32 | prune(cb: () => void): void; 33 | dispose(cb: (data: any) => void): void; 34 | decline(): void; 35 | invalidate(): void; 36 | on(event: string, cb: (...args: any[]) => void): void; 37 | }; 38 | readonly glob: (glob: string) => Record Promise>; 39 | } 40 | } 41 | 42 | // Vue 组件类型声明 43 | declare module "*.vue" { 44 | import type { DefineComponent } from "vue"; 45 | const component: DefineComponent<{}, {}, any>; 46 | export default component; 47 | } 48 | 49 | // Vue 宏命令类型声明 50 | declare module "vue" { 51 | import type { DefineComponent, Ref } from "vue"; 52 | 53 | // 生命周期钩子 54 | export declare const onMounted: (cb: () => void) => void; 55 | export declare const onBeforeMount: (cb: () => void) => void; 56 | export declare const onBeforeUnmount: (cb: () => void) => void; 57 | export declare const onUnmounted: (cb: () => void) => void; 58 | export declare const onActivated: (cb: () => void) => void; 59 | export declare const onDeactivated: (cb: () => void) => void; 60 | export declare const onBeforeUpdate: (cb: () => void) => void; 61 | export declare const onUpdated: (cb: () => void) => void; 62 | export declare const onErrorCaptured: (cb: (err: unknown) => void) => void; 63 | 64 | // 组合式 API 65 | export declare const createApp: any; 66 | export declare const ref: (value: T) => Ref; 67 | export declare const computed: (getter: () => T) => Ref; 68 | export declare const watch: typeof import("vue").watch; 69 | export declare const watchEffect: (effect: () => void) => void; 70 | export declare const reactive: (target: T) => T; 71 | export declare const readonly: (target: T) => Readonly; 72 | 73 | // 组件相关 74 | export declare const defineProps: { 75 | >(): Readonly; 76 | >(props: T): Readonly; 77 | }; 78 | 79 | export declare const defineEmits: { 80 | >(): T; 81 | >(emits: T): T; 82 | }; 83 | 84 | export declare const defineExpose: (exposed?: Record) => void; 85 | export declare const withDefaults: < 86 | Props, 87 | Defaults extends { [K in keyof Props]?: Props[K] }, 88 | >( 89 | props: Props, 90 | defaults: Defaults, 91 | ) => { 92 | [K in keyof Props]: K extends keyof Defaults ? Defaults[K] : Props[K]; 93 | }; 94 | } 95 | 96 | // 第三方模块声明 97 | declare module "vite" { 98 | import type { UserConfig, Plugin } from "vite"; 99 | 100 | export interface ViteConfig extends UserConfig { 101 | plugins?: Plugin[]; 102 | } 103 | 104 | export const defineConfig: (config: T) => T; 105 | } 106 | 107 | declare module "vue-router" { 108 | import type { Component } from "vue"; 109 | 110 | export interface RouteMeta { 111 | title?: string; 112 | description?: string; 113 | keywords?: string; 114 | transition?: string; 115 | requiresAuth?: boolean; 116 | [key: string]: any; 117 | } 118 | 119 | export interface RouteRecordRaw { 120 | path: string; 121 | name?: string; 122 | component?: Component | (() => Promise); 123 | components?: { [key: string]: Component }; 124 | redirect?: string | { name: string }; 125 | meta?: RouteMeta; 126 | children?: RouteRecordRaw[]; 127 | } 128 | 129 | export interface Router { 130 | push(to: string | { name: string; params?: any }): Promise; 131 | } 132 | 133 | export interface Route { 134 | meta: RouteMeta; 135 | params: Record; 136 | query: Record; 137 | hash: string; 138 | path: string; 139 | fullPath: string; 140 | matched: RouteRecordRaw[]; 141 | } 142 | 143 | // 添加 RouterLink 组件类型 144 | export interface RouterLinkProps { 145 | to: string | { name: string; params?: Record }; 146 | replace?: boolean; 147 | activeClass?: string; 148 | exactActiveClass?: string; 149 | custom?: boolean; 150 | ariaCurrentValue?: string; 151 | } 152 | 153 | export const RouterLink: Component; 154 | export const RouterView: Component; 155 | export const createRouter: any; 156 | export const createWebHistory: any; 157 | export const useRoute: () => Route; 158 | export const useRouter: () => Router; 159 | } 160 | 161 | declare module "@emailjs/browser" { 162 | const emailjs: any; 163 | export default emailjs; 164 | } 165 | 166 | declare module "vite-plugin-compression"; 167 | declare module "vite-plugin-image-optimizer"; 168 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 133 | 134 | 187 | -------------------------------------------------------------------------------- /src/components/layout/TheHeader.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 149 | 150 | 192 | -------------------------------------------------------------------------------- /src/views/tools/BookmarksView.vue: -------------------------------------------------------------------------------- 1 | 124 | 125 | 201 | -------------------------------------------------------------------------------- /src/views/SkillsView.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 145 | 146 | 195 | -------------------------------------------------------------------------------- /src/views/tools/TimestampView.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 222 | -------------------------------------------------------------------------------- /src/views/ToolsView.vue: -------------------------------------------------------------------------------- 1 | 110 | 111 | 219 | 220 | 302 | -------------------------------------------------------------------------------- /src/views/BlogView.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 277 | 278 | 314 | -------------------------------------------------------------------------------- /src/views/ContactView.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 298 | 299 | 317 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 165 | 166 | 493 | -------------------------------------------------------------------------------- /src/views/ProjectsView.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 268 | 269 | 415 | --------------------------------------------------------------------------------