feat: add initial project setup with Vue, Vite and Vuetify
This commit is contained in:
parent
01e7a57ed0
commit
361af96ba0
140
.gitignore
vendored
140
.gitignore
vendored
@ -1,11 +1,135 @@
|
|||||||
# ---> Vue
|
# Logs
|
||||||
# gitignore template for Vue.js projects
|
logs
|
||||||
#
|
*.log
|
||||||
# Recommended template: Node.gitignore
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# TODO: where does this rule come from?
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
docs/_book
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
# TODO: where does this rule come from?
|
# Runtime data
|
||||||
test/
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# vitepress build output
|
||||||
|
**/.vitepress/dist
|
||||||
|
|
||||||
|
# vitepress cache directory
|
||||||
|
**/.vitepress/cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
BIN
dist/assets/images/logo.png
vendored
Normal file
BIN
dist/assets/images/logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 248 KiB |
BIN
dist/assets/images/wechat-qr.png
vendored
Normal file
BIN
dist/assets/images/wechat-qr.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
1
dist/assets/index-B1oLPTqx.css
vendored
Normal file
1
dist/assets/index-B1oLPTqx.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3851
dist/assets/index-UMkeQYsC.js
vendored
Normal file
3851
dist/assets/index-UMkeQYsC.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dist/favicon.ico
vendored
Normal file
BIN
dist/favicon.ico
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
112
dist/index.html
vendored
Normal file
112
dist/index.html
vendored
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<!-- 核心元数据 -->
|
||||||
|
<title>Cat Tom | Try to do my best!</title>
|
||||||
|
<meta name="description" content="Cat Tom的个人主页,展示作品和联系方式">
|
||||||
|
|
||||||
|
<!-- 主题色 -->
|
||||||
|
<meta name="theme-color" content="#a8d8ea">
|
||||||
|
|
||||||
|
<!-- PWA配置 -->
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Cat Tom">
|
||||||
|
<link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png">
|
||||||
|
|
||||||
|
<!-- 预加载关键资源 -->
|
||||||
|
<link rel="preload" href="/assets/images/logo.png" as="image" type="image/png">
|
||||||
|
<link rel="preload" href="/assets/images/wechat-qr.png" as="image" type="image/png">
|
||||||
|
<link rel="preload" href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" as="style">
|
||||||
|
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||||
|
|
||||||
|
<!-- 字体加载策略 -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" media="print" onload="this.media='all'">
|
||||||
|
<noscript>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css">
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<!-- 默认图标 -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/icons/favicon.svg">
|
||||||
|
<link rel="alternate icon" href="/favicon.ico">
|
||||||
|
|
||||||
|
<!-- 社交分享优化 -->
|
||||||
|
<meta property="og:title" content="Cat Tom | Try to do my best!">
|
||||||
|
<meta property="og:description" content="Cat Tom的个人主页,展示作品和联系方式">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://yourdomain.com">
|
||||||
|
<meta property="og:image" content="/assets/images/social-share.png">
|
||||||
|
|
||||||
|
<!-- 防止搜索引擎索引开发环境 -->
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<script type="module" crossorigin src="/assets/index-UMkeQYsC.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-B1oLPTqx.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<!-- 初始加载动画 -->
|
||||||
|
<div class="app-loading">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<svg viewBox="0 0 50 50">
|
||||||
|
<circle cx="25" cy="25" r="20" fill="none" stroke="#7ac5e8" stroke-width="4"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 性能监控脚本 (仅开发环境) -->
|
||||||
|
|
||||||
|
<!-- 主应用脚本 -->
|
||||||
|
|
||||||
|
<!-- 加载状态样式 -->
|
||||||
|
<style>
|
||||||
|
.app-loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255,255,255,0.8);
|
||||||
|
z-index: 9999;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner svg {
|
||||||
|
animation: rotate 1.5s linear infinite;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner circle {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
stroke-linecap: round;
|
||||||
|
animation: dash 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
|
||||||
|
50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
|
||||||
|
100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当Vue挂载后隐藏加载动画 */
|
||||||
|
[data-v-app] + .app-loading {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
</html>
|
118
index.html
Normal file
118
index.html
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<!-- 核心元数据 -->
|
||||||
|
<title>Cat Tom | Try to do my best!</title>
|
||||||
|
<meta name="description" content="Cat Tom的个人主页,展示作品和联系方式">
|
||||||
|
|
||||||
|
<!-- 主题色 -->
|
||||||
|
<meta name="theme-color" content="#a8d8ea">
|
||||||
|
|
||||||
|
<!-- PWA配置 -->
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Cat Tom">
|
||||||
|
<link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png">
|
||||||
|
|
||||||
|
<!-- 预加载关键资源 -->
|
||||||
|
<link rel="preload" href="/assets/images/logo.png" as="image" type="image/png">
|
||||||
|
<link rel="preload" href="/assets/images/wechat-qr.png" as="image" type="image/png">
|
||||||
|
<link rel="preload" href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" as="style">
|
||||||
|
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||||
|
|
||||||
|
<!-- 字体加载策略 -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" media="print" onload="this.media='all'">
|
||||||
|
<noscript>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css">
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<!-- 默认图标 -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/assets/icons/favicon.svg">
|
||||||
|
<link rel="alternate icon" href="/favicon.ico">
|
||||||
|
|
||||||
|
<!-- 社交分享优化 -->
|
||||||
|
<meta property="og:title" content="Cat Tom | Try to do my best!">
|
||||||
|
<meta property="og:description" content="Cat Tom的个人主页,展示作品和联系方式">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://yourdomain.com">
|
||||||
|
<meta property="og:image" content="/assets/images/social-share.png">
|
||||||
|
|
||||||
|
<!-- 防止搜索引擎索引开发环境 -->
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<!-- 初始加载动画 -->
|
||||||
|
<div class="app-loading">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<svg viewBox="0 0 50 50">
|
||||||
|
<circle cx="25" cy="25" r="20" fill="none" stroke="#7ac5e8" stroke-width="4"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 性能监控脚本 (仅开发环境) -->
|
||||||
|
<script type="module">
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
import('vite-plugin-pwa/client').then(({ registerSW }) => {
|
||||||
|
registerSW({ immediate: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 主应用脚本 -->
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
|
||||||
|
<!-- 加载状态样式 -->
|
||||||
|
<style>
|
||||||
|
.app-loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255,255,255,0.8);
|
||||||
|
z-index: 9999;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner svg {
|
||||||
|
animation: rotate 1.5s linear infinite;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner circle {
|
||||||
|
stroke-dasharray: 90, 150;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
stroke-linecap: round;
|
||||||
|
animation: dash 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
|
||||||
|
50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
|
||||||
|
100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当Vue挂载后隐藏加载动画 */
|
||||||
|
[data-v-app] + .app-loading {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
</html>
|
2317
package-lock.json
generated
Normal file
2317
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "personalpage",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.cattom.site/cattom/PersonalPage.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
|
"@vueuse/core": "^13.0.0",
|
||||||
|
"gsap": "^3.12.7",
|
||||||
|
"remixicon": "^4.6.0",
|
||||||
|
"three": "^0.174.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vuetify": "^3.7.18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
"sass": "^1.86.0",
|
||||||
|
"sass-embedded": "^1.86.0",
|
||||||
|
"vite-plugin-vuetify": "^2.1.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/assets/images/logo.png
Normal file
BIN
public/assets/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 248 KiB |
BIN
public/assets/images/wechat-qr.png
Normal file
BIN
public/assets/images/wechat-qr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
132
src/App.vue
Normal file
132
src/App.vue
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 3D粒子背景 -->
|
||||||
|
<ParticleBackground />
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<main class="content">
|
||||||
|
<div class="profile-section">
|
||||||
|
<!-- 头像区域 -->
|
||||||
|
<div class="avatar-wrapper">
|
||||||
|
<img
|
||||||
|
src="/assets/images/logo.png"
|
||||||
|
alt="Cat Tom Avatar"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 打字机效果标题 -->
|
||||||
|
<Typewriter
|
||||||
|
:texts="['你好,我是Cat Tom', 'Web全栈开发者', '开源爱好者']"
|
||||||
|
:speed="100"
|
||||||
|
class="title"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 个人简介 -->
|
||||||
|
<p class="description">
|
||||||
|
专注于现代Web技术栈,擅长Vue/React全栈开发,开源项目贡献者
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 社交链接 -->
|
||||||
|
<SocialLinks
|
||||||
|
@show-wechat="showWechatModal = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 微信弹窗 -->
|
||||||
|
<WechatModal
|
||||||
|
v-if="showWechatModal"
|
||||||
|
@close="showWechatModal = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import ParticleBackground from './components/ParticleBackground.vue'
|
||||||
|
import SocialLinks from './components/SocialLinks.vue'
|
||||||
|
import Typewriter from './components/Typewriter.vue'
|
||||||
|
import WechatModal from './components/WechatModal.vue'
|
||||||
|
|
||||||
|
const showWechatModal = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
z-index: var(--z-index-content);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-section {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid var(--primary-light);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
238
src/components/ParticleBackground.vue
Normal file
238
src/components/ParticleBackground.vue
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<script setup>
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { useBreakpoints } from '@vueuse/core'
|
||||||
|
|
||||||
|
const canvasRef = ref(null)
|
||||||
|
|
||||||
|
// 响应式控制粒子数量
|
||||||
|
const breakpoints = useBreakpoints({
|
||||||
|
mobile: 640,
|
||||||
|
tablet: 1024
|
||||||
|
})
|
||||||
|
|
||||||
|
const isMobile = breakpoints.smaller('tablet')
|
||||||
|
const particleCount = isMobile.value ? 800 : 1500
|
||||||
|
|
||||||
|
// 动画参数
|
||||||
|
const config = {
|
||||||
|
particleSize: 1.5,
|
||||||
|
systemRadius: 15,
|
||||||
|
baseSpeed: 0.2,
|
||||||
|
hoverRadius: 3,
|
||||||
|
color: 0x7ac5e8,
|
||||||
|
lineColor: 0x5ab0d6,
|
||||||
|
lineDistance: 2.5
|
||||||
|
}
|
||||||
|
|
||||||
|
let scene, camera, renderer, particles, lines, mouse = { x: 0, y: 0 }
|
||||||
|
|
||||||
|
const initThreeJS = () => {
|
||||||
|
// 1. 初始化场景
|
||||||
|
scene = new THREE.Scene()
|
||||||
|
scene.background = new THREE.Color(0x000000)
|
||||||
|
scene.fog = new THREE.FogExp2(0x000000, 0.001)
|
||||||
|
|
||||||
|
// 2. 初始化相机
|
||||||
|
camera = new THREE.PerspectiveCamera(
|
||||||
|
75,
|
||||||
|
window.innerWidth / window.innerHeight,
|
||||||
|
0.1,
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
camera.position.z = 25
|
||||||
|
|
||||||
|
// 3. 初始化渲染器
|
||||||
|
renderer = new THREE.WebGLRenderer({
|
||||||
|
canvas: canvasRef.value,
|
||||||
|
antialias: true,
|
||||||
|
alpha: true
|
||||||
|
})
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
|
||||||
|
// 4. 创建粒子系统
|
||||||
|
createParticles()
|
||||||
|
|
||||||
|
// 5. 动画循环
|
||||||
|
animate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const createParticles = () => {
|
||||||
|
const geometry = new THREE.BufferGeometry()
|
||||||
|
const positions = new Float32Array(particleCount * 3)
|
||||||
|
const sizes = new Float32Array(particleCount)
|
||||||
|
|
||||||
|
// 初始化粒子位置
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
const i3 = i * 3
|
||||||
|
|
||||||
|
// 球体随机分布
|
||||||
|
const radius = config.systemRadius * Math.random()
|
||||||
|
const theta = Math.random() * Math.PI * 2
|
||||||
|
const phi = Math.acos(2 * Math.random() - 1)
|
||||||
|
|
||||||
|
positions[i3] = radius * Math.sin(phi) * Math.cos(theta)
|
||||||
|
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta)
|
||||||
|
positions[i3 + 2] = radius * Math.cos(phi)
|
||||||
|
|
||||||
|
sizes[i] = config.particleSize * (0.5 + Math.random() * 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
|
||||||
|
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1))
|
||||||
|
|
||||||
|
// 粒子材质
|
||||||
|
const material = new THREE.PointsMaterial({
|
||||||
|
color: config.color,
|
||||||
|
size: config.particleSize,
|
||||||
|
sizeAttenuation: true,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.8,
|
||||||
|
blending: THREE.AdditiveBlending
|
||||||
|
})
|
||||||
|
|
||||||
|
particles = new THREE.Points(geometry, material)
|
||||||
|
scene.add(particles)
|
||||||
|
|
||||||
|
// 创建连接线
|
||||||
|
createConnections(positions)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createConnections = (positions) => {
|
||||||
|
const lineGeometry = new THREE.BufferGeometry()
|
||||||
|
const linePositions = new Float32Array(particleCount * 3 * 2) // 每个粒子可能连接多个
|
||||||
|
|
||||||
|
// 简化的连接逻辑 (实际项目可以使用更高效的算法)
|
||||||
|
let lineIndex = 0
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
const i3 = i * 3
|
||||||
|
const p1 = new THREE.Vector3(
|
||||||
|
positions[i3],
|
||||||
|
positions[i3 + 1],
|
||||||
|
positions[i3 + 2]
|
||||||
|
)
|
||||||
|
|
||||||
|
// 只检查附近粒子
|
||||||
|
for (let j = i + 1; j < Math.min(i + 20, particleCount); j++) {
|
||||||
|
const j3 = j * 3
|
||||||
|
const p2 = new THREE.Vector3(
|
||||||
|
positions[j3],
|
||||||
|
positions[j3 + 1],
|
||||||
|
positions[j3 + 2]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (p1.distanceTo(p2) < config.lineDistance) {
|
||||||
|
linePositions[lineIndex++] = p1.x
|
||||||
|
linePositions[lineIndex++] = p1.y
|
||||||
|
linePositions[lineIndex++] = p1.z
|
||||||
|
linePositions[lineIndex++] = p2.x
|
||||||
|
linePositions[lineIndex++] = p2.y
|
||||||
|
linePositions[lineIndex++] = p2.z
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lineGeometry.setAttribute(
|
||||||
|
'position',
|
||||||
|
new THREE.BufferAttribute(linePositions, 3)
|
||||||
|
)
|
||||||
|
|
||||||
|
const lineMaterial = new THREE.LineBasicMaterial({
|
||||||
|
color: config.lineColor,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.15,
|
||||||
|
blending: THREE.AdditiveBlending
|
||||||
|
})
|
||||||
|
|
||||||
|
lines = new THREE.LineSegments(lineGeometry, lineMaterial)
|
||||||
|
scene.add(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
|
||||||
|
// 粒子动画
|
||||||
|
const positions = particles.geometry.attributes.position.array
|
||||||
|
const time = Date.now() * 0.0001 * config.baseSpeed
|
||||||
|
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
const i3 = i * 3
|
||||||
|
|
||||||
|
// 添加噪声运动
|
||||||
|
positions[i3] += Math.sin(time + i) * 0.01
|
||||||
|
positions[i3 + 1] += Math.cos(time + i * 0.5) * 0.01
|
||||||
|
positions[i3 + 2] += Math.sin(time * 0.5 + i) * 0.01
|
||||||
|
|
||||||
|
// 鼠标交互效果
|
||||||
|
const dx = positions[i3] - mouse.x * 20
|
||||||
|
const dy = positions[i3 + 1] - mouse.y * 20
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
|
||||||
|
if (distance < config.hoverRadius) {
|
||||||
|
positions[i3] += dx * 0.05
|
||||||
|
positions[i3 + 1] += dy * 0.05
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
particles.geometry.attributes.position.needsUpdate = true
|
||||||
|
renderer.render(scene, camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight
|
||||||
|
camera.updateProjectionMatrix()
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseMove = (event) => {
|
||||||
|
mouse.x = (event.clientX / window.innerWidth) * 2 - 1
|
||||||
|
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initThreeJS()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
window.addEventListener('mousemove', handleMouseMove)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
if (renderer) renderer.dispose()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<canvas
|
||||||
|
ref="canvasRef"
|
||||||
|
class="particle-canvas"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.particle-canvas {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: var(--z-index-particle);
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动设备降低不透明度 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.particle-canvas {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗黑模式适配 */
|
||||||
|
[data-theme="dark"] .particle-canvas {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
</style>
|
306
src/components/SocialLinks.vue
Normal file
306
src/components/SocialLinks.vue
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useMouseInElement } from '@vueuse/core'
|
||||||
|
import { useTheme } from 'vuetify'
|
||||||
|
|
||||||
|
// 主题控制
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
// 鼠标悬停效果
|
||||||
|
const cardRef = ref(null)
|
||||||
|
const { elementX, elementY } = useMouseInElement(cardRef)
|
||||||
|
|
||||||
|
// 社交平台数据配置
|
||||||
|
const socialPlatforms = [
|
||||||
|
{
|
||||||
|
name: 'GitHub',
|
||||||
|
icon: 'ri-github-fill',
|
||||||
|
url: 'https://github.com/cattom',
|
||||||
|
color: '#181717',
|
||||||
|
hoverColor: '#6e5494',
|
||||||
|
ariaLabel: '访问我的GitHub主页'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Twitter',
|
||||||
|
icon: 'ri-twitter-x-fill',
|
||||||
|
url: 'https://twitter.com/cattom',
|
||||||
|
color: '#000000',
|
||||||
|
hoverColor: '#1DA1F2',
|
||||||
|
ariaLabel: '在Twitter上关注我'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'LinkedIn',
|
||||||
|
icon: 'ri-linkedin-fill',
|
||||||
|
url: 'https://linkedin.com/in/cattom',
|
||||||
|
color: '#0A66C2',
|
||||||
|
hoverColor: '#0077B5',
|
||||||
|
ariaLabel: '查看我的LinkedIn资料'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Email',
|
||||||
|
icon: 'ri-mail-fill',
|
||||||
|
url: 'mailto:hi@cattom.me',
|
||||||
|
color: '#D44638',
|
||||||
|
hoverColor: '#EA4335',
|
||||||
|
ariaLabel: '发送电子邮件给我'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'WeChat',
|
||||||
|
icon: 'ri-wechat-fill',
|
||||||
|
color: '#07C160',
|
||||||
|
hoverColor: '#2DC100',
|
||||||
|
ariaLabel: '扫描我的微信二维码',
|
||||||
|
qrCode: '/assets/qr-wechat.png',
|
||||||
|
showQr: ref(false)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 计算卡片倾斜效果
|
||||||
|
const cardTransform = computed(() => {
|
||||||
|
const MAX_TILT = 8
|
||||||
|
const x = (elementX.value - (cardRef.value?.offsetWidth / 2 || 0)) / 20
|
||||||
|
const y = ((cardRef.value?.offsetHeight / 2 || 0) - elementY.value) / 20
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform: `
|
||||||
|
perspective(1000px)
|
||||||
|
rotateX(${Math.min(Math.max(-y, -MAX_TILT), MAX_TILT)}deg)
|
||||||
|
rotateY(${Math.min(Math.max(x, -MAX_TILT), MAX_TILT)}deg)
|
||||||
|
`,
|
||||||
|
transition: 'transform 0.5s cubic-bezier(0.03, 0.98, 0.52, 0.99)'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理二维码显示
|
||||||
|
const toggleQrCode = (platform) => {
|
||||||
|
if (!platform.qrCode) return
|
||||||
|
platform.showQr.value = !platform.showQr.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开外部链接
|
||||||
|
const openLink = (url) => {
|
||||||
|
if (!url) return
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="cardRef"
|
||||||
|
class="social-links-card"
|
||||||
|
:style="cardTransform"
|
||||||
|
>
|
||||||
|
<!-- 微信二维码弹窗 -->
|
||||||
|
<Transition name="fade">
|
||||||
|
<div
|
||||||
|
v-for="platform in socialPlatforms.filter(p => p.qrCode)"
|
||||||
|
v-show="platform.showQr?.value"
|
||||||
|
:key="`qr-${platform.name}`"
|
||||||
|
class="qr-overlay"
|
||||||
|
@click.self="platform.showQr.value = false"
|
||||||
|
>
|
||||||
|
<div class="qr-container">
|
||||||
|
<img
|
||||||
|
:src="platform.qrCode"
|
||||||
|
:alt="`${platform.name}二维码`"
|
||||||
|
class="qr-image"
|
||||||
|
>
|
||||||
|
<p class="qr-hint">扫码添加{{ platform.name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- 社交链接主体 -->
|
||||||
|
<div class="social-grid">
|
||||||
|
<div
|
||||||
|
v-for="(platform, index) in socialPlatforms"
|
||||||
|
:key="index"
|
||||||
|
class="social-item"
|
||||||
|
:style="{ '--hover-color': platform.hoverColor }"
|
||||||
|
@click="platform.qrCode ? toggleQrCode(platform) : openLink(platform.url)"
|
||||||
|
>
|
||||||
|
<div class="social-icon-wrapper">
|
||||||
|
<i
|
||||||
|
:class="platform.icon"
|
||||||
|
class="social-icon"
|
||||||
|
:aria-label="platform.ariaLabel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="social-name">{{ platform.name }}</span>
|
||||||
|
|
||||||
|
<!-- 悬停光效 -->
|
||||||
|
<div class="hover-light" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.social-links-card {
|
||||||
|
position: relative;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: rgba(var(--v-theme-surface), 0.8);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 30px rgba(0, 0, 0, 0.1),
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||||
|
will-change: transform;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
background 0.3s ease,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗黑模式适配 */
|
||||||
|
[data-theme="dark"] .social-links-card {
|
||||||
|
background: rgba(var(--v-theme-surface), 0.6);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 30px rgba(0, 0, 0, 0.3),
|
||||||
|
inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
||||||
|
gap: 1.2rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.8rem 0;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-item:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon-wrapper {
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(var(--v-theme-background), 0.7);
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-item:hover .social-icon {
|
||||||
|
color: var(--hover-color);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-name {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-item:hover .social-name {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停光效 */
|
||||||
|
.hover-light {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
var(--hover-color) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
|
z-index: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-item:hover .hover-light {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 二维码样式 */
|
||||||
|
.qr-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-container {
|
||||||
|
background: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-image {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-hint {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 过渡动画 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.social-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon-wrapper {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
163
src/components/Typewriter.vue
Normal file
163
src/components/Typewriter.vue
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
texts: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
validator: value => value.length > 0
|
||||||
|
},
|
||||||
|
typingSpeed: {
|
||||||
|
type: Number,
|
||||||
|
default: 100 // 毫秒/字符
|
||||||
|
},
|
||||||
|
deletingSpeed: {
|
||||||
|
type: Number,
|
||||||
|
default: 50 // 毫秒/字符
|
||||||
|
},
|
||||||
|
delayBetweenTexts: {
|
||||||
|
type: Number,
|
||||||
|
default: 1500 // 毫秒
|
||||||
|
},
|
||||||
|
cursorBlinkSpeed: {
|
||||||
|
type: Number,
|
||||||
|
default: 500 // 毫秒
|
||||||
|
},
|
||||||
|
loop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentText = ref('')
|
||||||
|
const isTyping = ref(true)
|
||||||
|
const isDeleting = ref(false)
|
||||||
|
const currentIndex = ref(0)
|
||||||
|
const cursorVisible = ref(true)
|
||||||
|
|
||||||
|
// 光标闪烁效果
|
||||||
|
let cursorInterval
|
||||||
|
onMounted(() => {
|
||||||
|
cursorInterval = setInterval(() => {
|
||||||
|
cursorVisible.value = !cursorVisible.value
|
||||||
|
}, props.cursorBlinkSpeed)
|
||||||
|
|
||||||
|
startTyping()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(cursorInterval)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打字机核心逻辑
|
||||||
|
let typingTimeout
|
||||||
|
const startTyping = () => {
|
||||||
|
const fullText = props.texts[currentIndex.value]
|
||||||
|
|
||||||
|
if (isTyping.value) {
|
||||||
|
// 打字阶段
|
||||||
|
currentText.value = fullText.substring(0, currentText.value.length + 1)
|
||||||
|
|
||||||
|
if (currentText.value === fullText) {
|
||||||
|
isTyping.value = false
|
||||||
|
typingTimeout = setTimeout(() => {
|
||||||
|
isDeleting.value = true
|
||||||
|
startTyping()
|
||||||
|
}, props.delayBetweenTexts)
|
||||||
|
} else {
|
||||||
|
typingTimeout = setTimeout(startTyping, props.typingSpeed)
|
||||||
|
}
|
||||||
|
} else if (isDeleting.value) {
|
||||||
|
// 删除阶段
|
||||||
|
currentText.value = fullText.substring(0, currentText.value.length - 1)
|
||||||
|
|
||||||
|
if (currentText.value === '') {
|
||||||
|
isDeleting.value = false
|
||||||
|
|
||||||
|
if (props.loop || currentIndex.value < props.texts.length - 1) {
|
||||||
|
currentIndex.value = (currentIndex.value + 1) % props.texts.length
|
||||||
|
isTyping.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
typingTimeout = setTimeout(startTyping, props.delayBetweenTexts / 2)
|
||||||
|
} else {
|
||||||
|
typingTimeout = setTimeout(startTyping, props.deletingSpeed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听文本变化
|
||||||
|
watch(() => props.texts, () => {
|
||||||
|
clearTimeout(typingTimeout)
|
||||||
|
currentText.value = ''
|
||||||
|
currentIndex.value = 0
|
||||||
|
isTyping.value = true
|
||||||
|
isDeleting.value = false
|
||||||
|
startTyping()
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="typewriter-container">
|
||||||
|
<span class="typewriter-text">{{ currentText }}</span>
|
||||||
|
<span
|
||||||
|
class="typewriter-cursor"
|
||||||
|
:class="{ 'cursor-visible': cursorVisible }"
|
||||||
|
aria-hidden="true"
|
||||||
|
>|</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.typewriter-container {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
font-family: 'Fira Code', 'Courier New', monospace;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typewriter-text {
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typewriter-cursor {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.1em;
|
||||||
|
color: var(--primary-color);
|
||||||
|
opacity: 0;
|
||||||
|
animation: cursorPulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 光标动画 */
|
||||||
|
@keyframes cursorPulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.typewriter-container {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗黑模式适配 */
|
||||||
|
[data-theme="dark"] .typewriter-text {
|
||||||
|
color: var(--text-primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .typewriter-cursor {
|
||||||
|
color: var(--primary-color-dark);
|
||||||
|
}
|
||||||
|
</style>
|
314
src/components/WechatModal.vue
Normal file
314
src/components/WechatModal.vue
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch, onUnmounted } from 'vue'
|
||||||
|
import { useEventListener } from '@vueuse/core'
|
||||||
|
import { useTheme } from 'vuetify'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
qrCode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validator: value => value.startsWith('/') || value.startsWith('http')
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '微信扫码添加'
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
type: String,
|
||||||
|
default: '打开微信扫一扫,添加我为好友'
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
type: Number,
|
||||||
|
default: 0 // 0表示不自动关闭
|
||||||
|
},
|
||||||
|
closeOnClickOutside: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
closeOnEsc: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:show', 'closed', 'opened'])
|
||||||
|
|
||||||
|
const theme = useTheme()
|
||||||
|
const modalRef = ref(null)
|
||||||
|
let autoCloseTimer = null
|
||||||
|
|
||||||
|
// 关闭模态框
|
||||||
|
const closeModal = () => {
|
||||||
|
clearTimeout(autoCloseTimer)
|
||||||
|
emit('update:show', false)
|
||||||
|
emit('closed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击外部关闭
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (props.closeOnClickOutside &&
|
||||||
|
modalRef.value &&
|
||||||
|
!modalRef.value.contains(event.target)) {
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESC键关闭
|
||||||
|
const handleKeydown = (event) => {
|
||||||
|
if (props.closeOnEsc && event.key === 'Escape') {
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动关闭定时器
|
||||||
|
const startAutoCloseTimer = () => {
|
||||||
|
if (props.duration > 0) {
|
||||||
|
autoCloseTimer = setTimeout(() => {
|
||||||
|
closeModal()
|
||||||
|
}, props.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听显示状态变化
|
||||||
|
watch(() => props.show, (val) => {
|
||||||
|
if (val) {
|
||||||
|
startAutoCloseTimer()
|
||||||
|
emit('opened')
|
||||||
|
|
||||||
|
// 禁止背景滚动
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
} else {
|
||||||
|
// 恢复背景滚动
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注册事件监听
|
||||||
|
useEventListener(document, 'mousedown', handleClickOutside)
|
||||||
|
useEventListener(document, 'keydown', handleKeydown)
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTimeout(autoCloseTimer)
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Transition name="wechat-modal">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="wechat-modal-mask"
|
||||||
|
:class="{ 'dark-mode': theme.global.name.value === 'dark' }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="modalRef"
|
||||||
|
class="wechat-modal-container"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
:aria-label="title"
|
||||||
|
>
|
||||||
|
<div class="wechat-modal-header">
|
||||||
|
<h3 class="modal-title">{{ title }}</h3>
|
||||||
|
<button
|
||||||
|
class="modal-close-btn"
|
||||||
|
@click="closeModal"
|
||||||
|
aria-label="关闭微信二维码弹窗"
|
||||||
|
>
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wechat-modal-body">
|
||||||
|
<div class="qr-code-wrapper">
|
||||||
|
<img
|
||||||
|
:src="qrCode"
|
||||||
|
alt="微信二维码"
|
||||||
|
class="qr-code-image"
|
||||||
|
loading="lazy"
|
||||||
|
@load="startAutoCloseTimer"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p class="qr-hint">{{ hint }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wechat-modal-footer">
|
||||||
|
<div class="scan-animation"></div>
|
||||||
|
<p class="footer-text">长按识别二维码</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wechat-modal-mask {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode.wechat-modal-mask {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-modal-container {
|
||||||
|
width: 320px;
|
||||||
|
background-color: var(--v-theme-surface);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
transform: scale(0.9);
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background-color: var(--v-theme-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
padding: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-modal-body {
|
||||||
|
padding: 24px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-wrapper {
|
||||||
|
padding: 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-image {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-hint {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-modal-footer {
|
||||||
|
padding: 12px 20px;
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-animation {
|
||||||
|
position: absolute;
|
||||||
|
top: -30px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 220px;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(var(--v-theme-primary), 0.8),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
border-radius: 100%;
|
||||||
|
animation: scan 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--v-theme-primary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scan {
|
||||||
|
0% {
|
||||||
|
top: -30px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
top: 190px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 190px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 过渡动画 */
|
||||||
|
.wechat-modal-enter-from,
|
||||||
|
.wechat-modal-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-modal-enter-from .wechat-modal-container,
|
||||||
|
.wechat-modal-leave-to .wechat-modal-container {
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-modal-enter-active .wechat-modal-container,
|
||||||
|
.wechat-modal-leave-active .wechat-modal-container {
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.wechat-modal-container {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-image {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-animation {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
204
src/composables/useParticles.js
Normal file
204
src/composables/useParticles.js
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// src/composables/useParticles.js
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRafFn } from '@vueuse/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 粒子系统组合式函数
|
||||||
|
* @param {Object} options - 配置选项
|
||||||
|
* @param {HTMLElement} options.canvas - 画布DOM元素
|
||||||
|
* @param {number} [options.density=100] - 粒子密度
|
||||||
|
* @param {string} [options.color='#ffffff'] - 粒子颜色
|
||||||
|
* @param {number} [options.minRadius=1] - 最小半径
|
||||||
|
* @param {number} [options.maxRadius=3] - 最大半径
|
||||||
|
* @param {number} [options.maxSpeed=0.5] - 最大速度
|
||||||
|
* @param {number} [options.lineLength=150] - 连线最大距离
|
||||||
|
* @param {boolean} [options.interactive=true] - 是否启用交互
|
||||||
|
* @param {number} [options.particleOpacity=0.7] - 粒子透明度
|
||||||
|
* @param {number} [options.lineOpacity=0.3] - 连线透明度
|
||||||
|
* @returns {Object} 控制方法
|
||||||
|
*/
|
||||||
|
export function useParticles(options) {
|
||||||
|
const {
|
||||||
|
canvas,
|
||||||
|
density = 100,
|
||||||
|
color = '#ffffff',
|
||||||
|
minRadius = 1,
|
||||||
|
maxRadius = 3,
|
||||||
|
maxSpeed = 0.5,
|
||||||
|
lineLength = 150,
|
||||||
|
interactive = true,
|
||||||
|
particleOpacity = 0.7,
|
||||||
|
lineOpacity = 0.3
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
const particles = []
|
||||||
|
const mouse = { x: null, y: null, radius: 100 }
|
||||||
|
const canvasSize = ref({ width: 0, height: 0 })
|
||||||
|
|
||||||
|
// 粒子类
|
||||||
|
class Particle {
|
||||||
|
constructor() {
|
||||||
|
this.x = Math.random() * canvasSize.value.width
|
||||||
|
this.y = Math.random() * canvasSize.value.height
|
||||||
|
this.size = Math.random() * (maxRadius - minRadius) + minRadius
|
||||||
|
this.density = Math.random() * 30 + 1
|
||||||
|
this.vx = Math.random() * maxSpeed * 2 - maxSpeed
|
||||||
|
this.vy = Math.random() * maxSpeed * 2 - maxSpeed
|
||||||
|
this.baseX = this.x
|
||||||
|
this.baseY = this.y
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.globalAlpha = particleOpacity
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
// 边界检测
|
||||||
|
if (this.x < 0 || this.x > canvasSize.value.width) {
|
||||||
|
this.vx = -this.vx
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.y < 0 || this.y > canvasSize.value.height) {
|
||||||
|
this.vy = -this.vy
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标交互
|
||||||
|
if (interactive) {
|
||||||
|
const dx = mouse.x - this.x
|
||||||
|
const dy = mouse.y - this.y
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
|
||||||
|
if (distance < mouse.radius) {
|
||||||
|
const forceDirectionX = dx / distance
|
||||||
|
const forceDirectionY = dy / distance
|
||||||
|
const force = (mouse.radius - distance) / mouse.radius * 2
|
||||||
|
const directionX = forceDirectionX * force * this.density
|
||||||
|
const directionY = forceDirectionY * force * this.density
|
||||||
|
|
||||||
|
this.x -= directionX
|
||||||
|
this.y -= directionY
|
||||||
|
} else {
|
||||||
|
if (this.x !== this.baseX) {
|
||||||
|
const dx = this.baseX - this.x
|
||||||
|
this.x += dx / 20
|
||||||
|
}
|
||||||
|
if (this.y !== this.baseY) {
|
||||||
|
const dy = this.baseY - this.y
|
||||||
|
this.y += dy / 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动粒子
|
||||||
|
this.x += this.vx
|
||||||
|
this.y += this.vy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化粒子
|
||||||
|
const initParticles = () => {
|
||||||
|
particles.length = 0
|
||||||
|
const particleCount = Math.floor((canvasSize.value.width * canvasSize.value.height) / 10000 * density)
|
||||||
|
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
particles.push(new Particle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制连线
|
||||||
|
const drawLines = () => {
|
||||||
|
for (let a = 0; a < particles.length; a++) {
|
||||||
|
for (let b = a; b < particles.length; b++) {
|
||||||
|
const dx = particles[a].x - particles[b].x
|
||||||
|
const dy = particles[a].y - particles[b].y
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
|
||||||
|
if (distance < lineLength) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.strokeStyle = color
|
||||||
|
ctx.globalAlpha = lineOpacity * (1 - distance / lineLength)
|
||||||
|
ctx.lineWidth = 0.5
|
||||||
|
ctx.moveTo(particles[a].x, particles[a].y)
|
||||||
|
ctx.lineTo(particles[b].x, particles[b].y)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动画循环
|
||||||
|
const animate = () => {
|
||||||
|
ctx.clearRect(0, 0, canvasSize.value.width, canvasSize.value.height)
|
||||||
|
|
||||||
|
particles.forEach(particle => {
|
||||||
|
particle.update()
|
||||||
|
particle.draw()
|
||||||
|
})
|
||||||
|
|
||||||
|
drawLines()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理鼠标移动
|
||||||
|
const handleMouseMove = (event) => {
|
||||||
|
if (!interactive) return
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
mouse.x = event.clientX - rect.left
|
||||||
|
mouse.y = event.clientY - rect.top
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理鼠标离开
|
||||||
|
const handleMouseOut = () => {
|
||||||
|
mouse.x = null
|
||||||
|
mouse.y = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理窗口大小变化
|
||||||
|
const handleResize = () => {
|
||||||
|
canvasSize.value.width = canvas.offsetWidth
|
||||||
|
canvasSize.value.height = canvas.offsetHeight
|
||||||
|
canvas.width = canvasSize.value.width
|
||||||
|
canvas.height = canvasSize.value.height
|
||||||
|
initParticles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动动画
|
||||||
|
const { pause, resume } = useRafFn(animate, { immediate: false })
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
handleResize()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
if (interactive) {
|
||||||
|
canvas.addEventListener('mousemove', handleMouseMove)
|
||||||
|
canvas.addEventListener('mouseout', handleMouseOut)
|
||||||
|
}
|
||||||
|
resume()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
pause()
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
if (interactive) {
|
||||||
|
canvas.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
canvas.removeEventListener('mouseout', handleMouseOut)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露控制方法
|
||||||
|
return {
|
||||||
|
pause,
|
||||||
|
resume,
|
||||||
|
updateConfig(newOptions) {
|
||||||
|
Object.assign(options, newOptions)
|
||||||
|
initParticles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
src/config/social-config.js
Normal file
44
src/config/social-config.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
export const socialLinks = [
|
||||||
|
{
|
||||||
|
id: 'qq',
|
||||||
|
name: 'QQ',
|
||||||
|
icon: 'ri-qq-line',
|
||||||
|
url: 'tencent://message/?uin=您的QQ号',
|
||||||
|
color: '#12B7F5'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wechat',
|
||||||
|
name: '微信',
|
||||||
|
icon: 'ri-wechat-line',
|
||||||
|
color: '#07C160',
|
||||||
|
action: 'openWechatModal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'github',
|
||||||
|
name: 'GitHub',
|
||||||
|
icon: 'ri-github-line',
|
||||||
|
url: 'https://github.com/您的账号',
|
||||||
|
color: '#181717'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gitea',
|
||||||
|
name: 'Gitea',
|
||||||
|
icon: 'ri-code-box-line',
|
||||||
|
url: 'https://gitea.com/您的账号',
|
||||||
|
color: '#609926'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'steam',
|
||||||
|
name: 'Steam',
|
||||||
|
icon: 'ri-steam-line',
|
||||||
|
url: 'https://steamcommunity.com/id/您的ID',
|
||||||
|
color: '#145B8E'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'email',
|
||||||
|
name: '邮箱',
|
||||||
|
icon: 'ri-mail-line',
|
||||||
|
url: 'mailto:您的邮箱',
|
||||||
|
color: '#D44638'
|
||||||
|
}
|
||||||
|
]
|
7
src/main.js
Normal file
7
src/main.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import vuetify from './plugins/vuetify' // 确保路径正确
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(vuetify)
|
||||||
|
app.mount('#app')
|
10
src/plugins/vuetify.js
Normal file
10
src/plugins/vuetify.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
import * as components from 'vuetify/components'
|
||||||
|
import * as directives from 'vuetify/directives'
|
||||||
|
export default createVuetify({
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
theme: {
|
||||||
|
defaultTheme: 'light'
|
||||||
|
}
|
||||||
|
})
|
141
src/styles/animations.css
Normal file
141
src/styles/animations.css
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/* src/styles/animations.css */
|
||||||
|
|
||||||
|
/* 粒子背景入场动画 */
|
||||||
|
@keyframes particles-fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 姓名文字渐变动画 */
|
||||||
|
@keyframes name-gradient-shift {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 社交图标悬停脉冲效果 */
|
||||||
|
@keyframes social-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(122, 197, 232, 0.4);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 12px rgba(122, 197, 232, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(122, 197, 232, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 微信二维码弹窗动画 */
|
||||||
|
@keyframes modal-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 光标闪烁动画 */
|
||||||
|
@keyframes blink-caret {
|
||||||
|
from, to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景粒子浮动效果 */
|
||||||
|
@keyframes particle-float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 应用动画的CSS类 */
|
||||||
|
.particle-container {
|
||||||
|
animation: particles-fade-in 1.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-gradient-animation {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#a8d8ea 0%,
|
||||||
|
#7ac5e8 50%,
|
||||||
|
#56b4d3 100%
|
||||||
|
);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: name-gradient-shift 8s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon-hover {
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon-hover:hover {
|
||||||
|
animation: social-pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-modal-content {
|
||||||
|
animation: modal-fade-in 0.4s cubic-bezier(0.23, 1, 0.32, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typewriter-cursor {
|
||||||
|
animation: blink-caret 0.75s step-end infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-float {
|
||||||
|
animation: particle-float 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式动画调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
@keyframes name-gradient-shift {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 30%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 30%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-modal-content {
|
||||||
|
animation-name: modal-fade-in-mobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-fade-in-mobile {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px) scale(0.98);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 性能优化设置 */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
136
src/styles/base.css
Normal file
136
src/styles/base.css
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/* ========== 基础重置 ========== */
|
||||||
|
@import url('https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap');
|
||||||
|
|
||||||
|
:where(*, *::before, *::after) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(html) {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(body) {
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
'Noto Sans SC',
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(at 80% 20%, var(--color-primary-100) 0px, transparent 50%),
|
||||||
|
radial-gradient(at 0% 50%, var(--color-primary-50) 0px, transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 排版系统 ========== */
|
||||||
|
:where(h1, h2, h3, h4, h5, h6) {
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(h1) { font-size: var(--text-4xl); }
|
||||||
|
:where(h2) { font-size: var(--text-3xl); }
|
||||||
|
:where(h3) { font-size: var(--text-2xl); }
|
||||||
|
:where(h4) { font-size: var(--text-xl); }
|
||||||
|
:where(p) { margin-bottom: var(--spacing-md); }
|
||||||
|
|
||||||
|
/* ========== 交互元素 ========== */
|
||||||
|
:where(a) {
|
||||||
|
color: var(--color-primary-500);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(a:hover) {
|
||||||
|
color: var(--color-primary-600);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(button) {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(button:focus-visible) {
|
||||||
|
outline: 2px solid var(--color-primary-400);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 实用类 ========== */
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: var(--spacing-xl) 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 动画基础 ========== */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-12px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 猫咪主题元素 ========== */
|
||||||
|
.cat-paw {
|
||||||
|
position: absolute;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-image: url('@/assets/paw-print.png');
|
||||||
|
background-size: contain;
|
||||||
|
opacity: 0.15;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: var(--z-index-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 响应式调整 ========== */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
:where(h1) { font-size: var(--text-3xl); }
|
||||||
|
:where(h2) { font-size: var(--text-2xl); }
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: var(--spacing-lg) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 打印样式 ========== */
|
||||||
|
@media print {
|
||||||
|
:where(body) {
|
||||||
|
background: none !important;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(a) {
|
||||||
|
color: black !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
}
|
6
src/styles/settings.scss
Normal file
6
src/styles/settings.scss
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// src/styles/settings.scss
|
||||||
|
@use "vuetify/settings" with (
|
||||||
|
$color-pack: false,
|
||||||
|
$body-font-family: 'Roboto',
|
||||||
|
$border-radius-root: 8px
|
||||||
|
);
|
107
src/styles/variables.css
Normal file
107
src/styles/variables.css
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/* ========== 颜色变量 ========== */
|
||||||
|
:root {
|
||||||
|
/* 主色调 (猫主题蓝) */
|
||||||
|
--color-primary-50: #f0f9ff;
|
||||||
|
--color-primary-100: #e0f2fe;
|
||||||
|
--color-primary-200: #bae6fd;
|
||||||
|
--color-primary-300: #7ac5e8; /* 主要品牌色 */
|
||||||
|
--color-primary-400: #38bdf8;
|
||||||
|
--color-primary-500: #0ea5e9;
|
||||||
|
--color-primary-600: #0284c7;
|
||||||
|
|
||||||
|
/* 文字颜色 */
|
||||||
|
--color-text-primary: #1e293b; /* 主要文字 */
|
||||||
|
--color-text-secondary: #64748b; /* 次要文字 */
|
||||||
|
--color-text-inverse: #f8fafc; /* 反色文字 */
|
||||||
|
|
||||||
|
/* 背景色 */
|
||||||
|
--color-bg-light: #f8fafc; /* 浅色背景 */
|
||||||
|
--color-bg-dark: #1e293b; /* 深色背景 */
|
||||||
|
--color-bg-blur: rgba(255, 255, 255, 0.85); /* 毛玻璃效果 */
|
||||||
|
|
||||||
|
/* 社交平台品牌色 */
|
||||||
|
--social-qq: #12b7f5;
|
||||||
|
--social-wechat: #07c160;
|
||||||
|
--social-github: #181717;
|
||||||
|
--social-gitea: #609926;
|
||||||
|
--social-steam: #145b8e;
|
||||||
|
--social-email: #d44638;
|
||||||
|
--social-discord: #5865f2;
|
||||||
|
|
||||||
|
/* 状态色 */
|
||||||
|
--color-success: #10b981;
|
||||||
|
--color-warning: #f59e0b;
|
||||||
|
--color-error: #ef4444;
|
||||||
|
--color-info: #3b82f6;
|
||||||
|
|
||||||
|
/* ========== 尺寸变量 ========== */
|
||||||
|
--spacing-xs: 0.25rem; /* 4px */
|
||||||
|
--spacing-sm: 0.5rem; /* 8px */
|
||||||
|
--spacing-md: 1rem; /* 16px */
|
||||||
|
--spacing-lg: 1.5rem; /* 24px */
|
||||||
|
--spacing-xl: 2rem; /* 32px */
|
||||||
|
|
||||||
|
--radius-sm: 0.25rem; /* 4px */
|
||||||
|
--radius-md: 0.5rem; /* 8px */
|
||||||
|
--radius-lg: 1rem; /* 16px */
|
||||||
|
--radius-full: 9999px; /* 圆形 */
|
||||||
|
|
||||||
|
/* ========== 文字变量 ========== */
|
||||||
|
--text-xs: 0.75rem; /* 12px */
|
||||||
|
--text-sm: 0.875rem; /* 14px */
|
||||||
|
--text-base: 1rem; /* 16px */
|
||||||
|
--text-lg: 1.125rem; /* 18px */
|
||||||
|
--text-xl: 1.25rem; /* 20px */
|
||||||
|
--text-2xl: 1.5rem; /* 24px */
|
||||||
|
--text-3xl: 1.875rem; /* 30px */
|
||||||
|
--text-4xl: 2.25rem; /* 36px */
|
||||||
|
--text-5xl: 3rem; /* 48px */
|
||||||
|
|
||||||
|
/* ========== 阴影变量 ========== */
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
/* ========== 动效变量 ========== */
|
||||||
|
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--transition-normal: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
/* ========== 层级管理 ========== */
|
||||||
|
--z-index-particle: 0;
|
||||||
|
--z-index-background: 1;
|
||||||
|
--z-index-content: 10;
|
||||||
|
--z-index-social: 20;
|
||||||
|
--z-index-header: 30;
|
||||||
|
--z-index-modal: 100;
|
||||||
|
--z-index-toast: 200;
|
||||||
|
|
||||||
|
/* ========== 响应式断点 ========== */
|
||||||
|
--screen-xs: 480px;
|
||||||
|
--screen-sm: 640px;
|
||||||
|
--screen-md: 768px;
|
||||||
|
--screen-lg: 1024px;
|
||||||
|
--screen-xl: 1280px;
|
||||||
|
--screen-2xl: 1536px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 暗黑模式变量 ========== */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-text-primary: #f8fafc;
|
||||||
|
--color-text-secondary: #94a3b8;
|
||||||
|
--color-bg-light: #1e293b;
|
||||||
|
--color-bg-dark: #0f172a;
|
||||||
|
--color-bg-blur: rgba(15, 23, 42, 0.85);
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.5);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.6),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.4);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.7),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
12
src/vuetity.js
Normal file
12
src/vuetity.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// src/plugins/vuetify.js
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
import * as components from 'vuetify/components'
|
||||||
|
import * as directives from 'vuetify/directives'
|
||||||
|
|
||||||
|
export default createVuetify({
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
theme: {
|
||||||
|
defaultTheme: 'light'
|
||||||
|
}
|
||||||
|
})
|
30
vite.config.js
Normal file
30
vite.config.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// vite.config.js
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue' // 注意这里是Vue 3的插件
|
||||||
|
import VuetifyPlugin from 'vite-plugin-vuetify'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
VuetifyPlugin({
|
||||||
|
autoImport: true,
|
||||||
|
styles: {
|
||||||
|
configFile: 'src/styles/settings.scss'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: `
|
||||||
|
@use "vuetify/styles" as *;
|
||||||
|
@use "@/styles/settings" as *;
|
||||||
|
`,
|
||||||
|
charset: false
|
||||||
|
},
|
||||||
|
sass: {
|
||||||
|
implementation: 'sass-embedded'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user