Initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
/node_modules
|
||||
/.claude
|
||||
/dist
|
||||
agent.md
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- SEO Meta -->
|
||||
<title>Nanami — 个人主页</title>
|
||||
<meta name="description" content="Madobi Nanami 的个人主页 — 计算机技术博主 | 全栈开发 | 开源爱好者。探索技术博客、项目作品与更多内容。">
|
||||
<meta name="keywords" content="Nanami, Madobi Nanami, 技术博客, 全栈开发, 个人主页, 开源项目, 程序员, 开发者, Go, Vue, C++, Python, Rust">
|
||||
<meta name="author" content="Madobi Nanami">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://nanami.tech/">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="Nanami — 个人主页">
|
||||
<meta property="og:description" content="Madobi Nanami 的个人主页 — 计算机技术博主 | 全栈开发 | 开源爱好者">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://nanami.tech/">
|
||||
<meta property="og:site_name" content="Nanami's Homepage">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="Nanami — 个人主页">
|
||||
<meta name="twitter:description" content="Madobi Nanami 的个人主页 — 计算机技术博主 | 全栈开发 | 开源爱好者">
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
background-color: #f8f8f6;
|
||||
color: #1a1a1c;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
#app {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1365
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "nanami-homepage",
|
||||
"version": "1.0.0",
|
||||
"description": "Personal homepage of Madobi Nanami - Computer tech blogger",
|
||||
"main": "index.html",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.nanami.tech/NanamiWork/NanamiHomePage.git"
|
||||
},
|
||||
"keywords": [
|
||||
"personal-homepage",
|
||||
"blog",
|
||||
"portfolio",
|
||||
"tech-blog"
|
||||
],
|
||||
"author": "Madobi Nanami <i@nanami.tech>",
|
||||
"license": "ISC"
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div :class="{ 'dark': isDarkMode }">
|
||||
<div class="bg-grid"></div>
|
||||
<TopNav />
|
||||
<main class="main-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="page-fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</main>
|
||||
<PageFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { provide } from 'vue';
|
||||
import { useTheme } from './utils/toggleThemes';
|
||||
import TopNav from './components/TopNav.vue';
|
||||
import PageFooter from './components/PageFooter.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
TopNav,
|
||||
PageFooter
|
||||
},
|
||||
setup() {
|
||||
const { isDarkMode, toggleTheme } = useTheme();
|
||||
|
||||
provide('isDarkMode', isDarkMode);
|
||||
provide('toggleTheme', toggleTheme);
|
||||
|
||||
return {
|
||||
isDarkMode,
|
||||
toggleTheme
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './styles/themes.css';
|
||||
@import './styles/common.css';
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.47;
|
||||
letter-spacing: -0.374px;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
min-height: calc(100vh - 280px);
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
/* Page transition */
|
||||
.page-fade-enter-active,
|
||||
.page-fade-leave-active {
|
||||
transition: opacity 0.25s ease, transform 0.25s ease;
|
||||
}
|
||||
|
||||
.page-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
.page-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color-strong);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<section class="about section">
|
||||
<div class="container">
|
||||
<ScrollReveal>
|
||||
<h2 class="section-title">{{ title }}</h2>
|
||||
<p class="section-subtitle">{{ subtitle }}</p>
|
||||
</ScrollReveal>
|
||||
|
||||
<div class="about-grid">
|
||||
<!-- Bio -->
|
||||
<ScrollReveal :delay="100">
|
||||
<div class="card about-bio">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-user-astronaut"></i> 个人简介</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p v-for="(paragraph, i) in bio" :key="i" class="bio-text">{{ paragraph }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
<!-- Personal Tags -->
|
||||
<ScrollReveal :delay="200">
|
||||
<div class="card about-tags">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-tags"></i> 个人标签</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tags-wrap">
|
||||
<span v-for="tag in personalTags" :key="tag" class="tag accent">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
<!-- Tech Stack -->
|
||||
<ScrollReveal :delay="300">
|
||||
<div class="card about-tech">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-microchip"></i> 技术栈</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-for="group in techStack" :key="group.category" class="tech-group">
|
||||
<h4 class="tech-category">{{ group.category }}</h4>
|
||||
<div class="tags-wrap">
|
||||
<span v-for="tech in group.items" :key="tech.name" class="tech-tag">
|
||||
<i :class="tech.icon" class="tech-icon"></i>
|
||||
<span>{{ tech.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScrollReveal from './ScrollReveal.vue';
|
||||
|
||||
export default {
|
||||
name: 'AboutSection',
|
||||
components: {
|
||||
ScrollReveal
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '关于我'
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '一名热衷于计算机技术和互联网的全栈开发者'
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const bio = [
|
||||
'你好,我是 Madobi Nanami,一名热衷于计算机技术和互联网的全栈开发者。热衷于各类计算机技术的探索与实践。主要研究方向为网络工程、云原生运维。前后端开发也在作为我的一个兴趣爱好之一发展。',
|
||||
'我十分喜欢折腾 HomeLab, 搭建各种企业级服务和应用,探索各种技术的边界与可能性。通过实践,我积累了丰富的经验,并不断提升自己的技术能力。',
|
||||
'在工作之余,我热衷于开源社区贡献,撰写技术博客,分享我的经验和见解。我相信技术的力量可以改变世界,也希望通过我的努力为开源社区和技术爱好者们带来价值。'
|
||||
];
|
||||
|
||||
const personalTags = [
|
||||
'全栈开发者', '开源爱好者', '技术宅', 'Linux Lover',
|
||||
'云原生运维', '网络工程师', 'Galgame Lover', 'Anime Lover', 'HiFi Player'
|
||||
];
|
||||
|
||||
const techStack = [
|
||||
{
|
||||
category: '后端开发',
|
||||
items: [
|
||||
{ name: 'C++', icon: 'fas fa-code' },
|
||||
{ name: 'Go', icon: 'fas fa-cog' },
|
||||
{ name: 'Node.js', icon: 'fab fa-node-js' }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: '前端开发',
|
||||
items: [
|
||||
{ name: 'Vue.js', icon: 'fas fa-code' },
|
||||
{ name: 'JavaScript', icon: 'fas fa-code' },
|
||||
{ name: 'HTML/CSS', icon: 'fab fa-html5' }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: '基础设施',
|
||||
items: [
|
||||
{ name: 'Linux', icon: 'fab fa-linux' },
|
||||
{ name: 'Windows Server', icon: 'fab fa-windows' },
|
||||
{ name: 'Nginx', icon: 'fas fa-server' },
|
||||
{ name: 'Docker', icon: 'fab fa-docker' },
|
||||
{ name: 'Kubernetes', icon: 'fas fa-cubes' },
|
||||
{ name: 'Git', icon: 'fab fa-git-alt' }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: '数据存储',
|
||||
items: [
|
||||
{ name: 'MySQL', icon: 'fas fa-database' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
bio,
|
||||
personalTags,
|
||||
techStack
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.about {
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.about-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.about-bio {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.about-bio .card-header h3,
|
||||
.about-tags .card-header h3,
|
||||
.about-tech .card-header h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.about-bio .card-header h3 i,
|
||||
.about-tags .card-header h3 i,
|
||||
.about-tech .card-header h3 i {
|
||||
color: var(--accent);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.bio-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.tags-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Tech Stack */
|
||||
.tech-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tech-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tech-category {
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0 0 8px 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.tech-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
background: var(--tag-bg);
|
||||
transition: all 0.2s;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tech-tag:hover {
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.tech-icon {
|
||||
font-size: 13px;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.tech-tag:hover .tech-icon {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Card header icon */
|
||||
.about-bio .card-header h3 i,
|
||||
.about-tags .card-header h3 i,
|
||||
.about-tech .card-header h3 i {
|
||||
color: var(--accent);
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.about {
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.about-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,414 @@
|
||||
<template>
|
||||
<section class="contact section">
|
||||
<div class="container">
|
||||
<ScrollReveal>
|
||||
<h2 class="section-title">{{ title }}</h2>
|
||||
<p class="section-subtitle">{{ subtitle }}</p>
|
||||
</ScrollReveal>
|
||||
|
||||
<div class="contact-grid">
|
||||
<!-- Contact Info -->
|
||||
<ScrollReveal :delay="100">
|
||||
<div class="contact-info">
|
||||
<div class="card contact-info-card">
|
||||
<h3><i class="fas fa-paper-plane"></i> 联系方式</h3>
|
||||
<div class="contact-list">
|
||||
<a
|
||||
v-for="item in contactItems"
|
||||
:key="item.label"
|
||||
:href="item.href"
|
||||
class="contact-item"
|
||||
:target="item.external ? '_blank' : '_self'"
|
||||
:rel="item.external ? 'noopener noreferrer' : undefined"
|
||||
>
|
||||
<div class="contact-item-icon">
|
||||
<i :class="item.icon" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="contact-item-text">
|
||||
<span class="contact-item-label">{{ item.label }}</span>
|
||||
<span class="contact-item-value">{{ item.value }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card social-card">
|
||||
<h3><i class="fas fa-share-nodes"></i> 社交媒体</h3>
|
||||
<div class="social-grid">
|
||||
<a
|
||||
v-for="social in socialLinks"
|
||||
:key="social.label"
|
||||
:href="social.url"
|
||||
:aria-label="social.label"
|
||||
:title="social.label"
|
||||
class="social-item"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i :class="social.icon" aria-hidden="true"></i>
|
||||
<span>{{ social.label }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
<!-- Contact Form -->
|
||||
<ScrollReveal :delay="200">
|
||||
<div class="card contact-form-card">
|
||||
<h3><i class="fas fa-envelope"></i> 发送消息</h3>
|
||||
<form class="contact-form" @submit.prevent="handleSubmit">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="contact-name">昵称</label>
|
||||
<input
|
||||
id="contact-name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
placeholder="你的昵称"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="contact-email">邮箱</label>
|
||||
<input
|
||||
id="contact-email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="contact-subject">主题</label>
|
||||
<input
|
||||
id="contact-subject"
|
||||
v-model="form.subject"
|
||||
type="text"
|
||||
placeholder="消息主题"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="contact-message">内容</label>
|
||||
<textarea
|
||||
id="contact-message"
|
||||
v-model="form.message"
|
||||
rows="5"
|
||||
placeholder="写下你想说的内容..."
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" :disabled="submitted">
|
||||
<i :class="submitted ? 'fas fa-check' : 'fas fa-paper-plane'"></i>
|
||||
{{ submitted ? '已发送' : '发送消息' }}
|
||||
</button>
|
||||
<p v-if="submitted" class="form-note success">感谢你的消息!我会尽快回复。</p>
|
||||
<p class="form-note hint">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
或直接发送邮件至 <a href="mailto:i@nanami.tech">i@nanami.tech</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive } from 'vue';
|
||||
import ScrollReveal from './ScrollReveal.vue';
|
||||
|
||||
export default {
|
||||
name: 'ContactSection',
|
||||
components: {
|
||||
ScrollReveal
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '联系我'
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '欢迎技术交流、合作咨询或友情链接'
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const submitted = ref(false);
|
||||
const form = reactive({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
});
|
||||
|
||||
const contactItems = [
|
||||
{
|
||||
label: '电子邮箱',
|
||||
value: 'contact@nanami.tech',
|
||||
icon: 'fas fa-envelope',
|
||||
href: 'mailto:contact@nanami.tech',
|
||||
external: false
|
||||
},
|
||||
{
|
||||
label: 'Gitea',
|
||||
value: 'gitea.nanami.tech/NanamiAdmin',
|
||||
icon: 'fab fa-github',
|
||||
href: 'https://gitea.nanami.tech/NanamiAdmin',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: '技术博客',
|
||||
value: 'blog.nanami.tech',
|
||||
icon: 'fas fa-blog',
|
||||
href: 'https://blog.nanami.tech',
|
||||
external: true
|
||||
}
|
||||
];
|
||||
|
||||
const socialLinks = [
|
||||
{ label: 'Gitea', icon: 'fab fa-github', url: 'https://gitea.nanami.tech/NanamiAdmin' },
|
||||
{ label: 'Telegram', icon: 'fab fa-telegram', url: 'https://t.me/MadobiNanami' },
|
||||
{ label: 'Email', icon: 'fas fa-envelope', url: 'mailto:contact@nanami.tech' }
|
||||
];
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Static form — show success state since no backend
|
||||
submitted.value = true;
|
||||
setTimeout(() => {
|
||||
submitted.value = false;
|
||||
form.name = '';
|
||||
form.email = '';
|
||||
form.subject = '';
|
||||
form.message = '';
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
return {
|
||||
form,
|
||||
submitted,
|
||||
contactItems,
|
||||
socialLinks,
|
||||
handleSubmit
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contact {
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.contact-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Contact Info */
|
||||
.contact-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.contact-info-card h3,
|
||||
.social-card h3,
|
||||
.contact-form-card h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.contact-info-card h3 i,
|
||||
.social-card h3 i,
|
||||
.contact-form-card h3 i {
|
||||
color: var(--accent);
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contact-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.contact-item:hover {
|
||||
background: var(--surface-light);
|
||||
}
|
||||
|
||||
.contact-item-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-item-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.contact-item-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.contact-item-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
word-break: break-all;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
/* Social */
|
||||
.social-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.social-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.social-item:hover {
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.social-item i {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.contact-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
padding: 10px 14px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 1.47;
|
||||
background: var(--surface-light);
|
||||
color: var(--text-color);
|
||||
transition: all 0.3s;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-subtle);
|
||||
}
|
||||
|
||||
.form-group input::placeholder,
|
||||
.form-group textarea::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.form-note {
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.form-note.success {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-note.hint {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.form-note.hint a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.contact {
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.contact-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.social-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<section class="hero section">
|
||||
<div class="container hero-inner">
|
||||
<div class="hero-avatar-wrap">
|
||||
<div class="hero-avatar-ring">
|
||||
<div class="hero-avatar">
|
||||
<img
|
||||
src="https://dl-allfiles.nanami.tech/img/avatar.jpg"
|
||||
alt="Nanami's avatar"
|
||||
class="avatar-img"
|
||||
@error="onAvatarError"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-text">
|
||||
<h1 class="hero-name">Madobi <span class="hero-name-accent">Nanami</span></h1>
|
||||
<p class="hero-slogan">
|
||||
<span class="slogan-icon"><i class="fas fa-terminal"></i></span>
|
||||
Full Stack Developer | Open Source Enthusiast | Tech Explorer
|
||||
</p>
|
||||
<p class="hero-bio">
|
||||
Tech flows never end!
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<router-link to="/projects" class="btn btn-primary">
|
||||
<i class="fas fa-code"></i> 查看项目
|
||||
</router-link>
|
||||
<router-link to="/about" class="btn btn-outline">
|
||||
<i class="fas fa-user"></i> 了解更多
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'HeroSection',
|
||||
setup() {
|
||||
const avatarError = ref(false);
|
||||
|
||||
const onAvatarError = () => {
|
||||
avatarError.value = true;
|
||||
};
|
||||
|
||||
return {
|
||||
avatarError,
|
||||
onAvatarError
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hero {
|
||||
padding: 80px 0 64px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(32px, 6vw, 64px);
|
||||
}
|
||||
|
||||
/* Avatar */
|
||||
.hero-avatar-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hero-avatar-ring {
|
||||
position: relative;
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(
|
||||
var(--accent),
|
||||
var(--accent),
|
||||
transparent 60%,
|
||||
transparent 70%,
|
||||
var(--accent) 80%,
|
||||
var(--accent)
|
||||
);
|
||||
animation: avatarSpin 8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes avatarSpin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.hero-avatar {
|
||||
width: clamp(96px, 15vw, 128px);
|
||||
height: clamp(96px, 15vw, 128px);
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: var(--surface-light);
|
||||
border: 3px solid var(--card-bg);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Text */
|
||||
.hero-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
font-family: 'SF Pro Display', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: clamp(28px, 5vw, 44px);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 12px 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.hero-name-accent {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.hero-slogan {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: clamp(15px, 2vw, 18px);
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 12px 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.slogan-icon {
|
||||
color: var(--accent);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hero-bio {
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: clamp(14px, 1.8vw, 16px);
|
||||
line-height: 1.6;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0 0 24px 0;
|
||||
max-width: 520px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.hero {
|
||||
padding: 48px 0 40px;
|
||||
}
|
||||
|
||||
.hero-inner {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-avatar-ring {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero-slogan {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-bio {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<section class="links section">
|
||||
<div class="container">
|
||||
<ScrollReveal>
|
||||
<h2 class="section-title">探索更多</h2>
|
||||
<p class="section-subtitle">发现我的技术博客、项目作品与资源分享</p>
|
||||
</ScrollReveal>
|
||||
|
||||
<div class="links-grid">
|
||||
<ScrollReveal
|
||||
v-for="(card, index) in linkCards"
|
||||
:key="card.label"
|
||||
:delay="index * 100"
|
||||
>
|
||||
<a :href="card.url" class="link-card card" :target="card.external ? '_blank' : '_self'" :rel="card.external ? 'noopener noreferrer' : undefined">
|
||||
<div class="link-card-icon" :class="card.accent">
|
||||
<i :class="card.icon" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="link-card-content">
|
||||
<h3 class="link-card-title">{{ card.label }}</h3>
|
||||
<p class="link-card-desc">{{ card.description }}</p>
|
||||
</div>
|
||||
<div class="link-card-arrow">
|
||||
<i class="fas fa-arrow-right" aria-hidden="true"></i>
|
||||
</div>
|
||||
</a>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScrollReveal from './ScrollReveal.vue';
|
||||
|
||||
export default {
|
||||
name: 'LinksSection',
|
||||
components: {
|
||||
ScrollReveal
|
||||
},
|
||||
setup() {
|
||||
const linkCards = [
|
||||
{
|
||||
label: '技术博客',
|
||||
description: '阅读技术文章、开发笔记与心得体会',
|
||||
icon: 'fas fa-blog',
|
||||
url: 'https://blog.nanami.tech',
|
||||
external: true,
|
||||
accent: 'accent-teal'
|
||||
},
|
||||
{
|
||||
label: '个人网盘',
|
||||
description: '访问开源资源、工具软件与文件分享',
|
||||
icon: 'fas fa-cloud',
|
||||
url: 'https://drive.nanami.tech',
|
||||
external: true,
|
||||
accent: 'accent-warm'
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
description: '浏览我的开源项目与代码贡献',
|
||||
icon: 'fab fa-github',
|
||||
url: 'https://github.com/MadobiNanami',
|
||||
external: true,
|
||||
accent: 'accent-mono'
|
||||
},
|
||||
{
|
||||
label: '项目作品',
|
||||
description: '查看完整的项目展示与技术栈详情',
|
||||
icon: 'fas fa-code',
|
||||
url: '/projects',
|
||||
external: false,
|
||||
accent: 'accent-teal'
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
linkCards
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.links {
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.links-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.link-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
transform: scaleY(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.link-card:hover::before {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.link-card.accent-teal::before { background: var(--accent); }
|
||||
.link-card.accent-warm::before { background: #c7784a; }
|
||||
.link-card.accent-mono::before { background: var(--text-color); }
|
||||
|
||||
.link-card-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.link-card:hover .link-card-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.accent-teal .link-card-icon {
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.accent-warm .link-card-icon {
|
||||
background: rgba(199, 120, 74, 0.1);
|
||||
color: #c7784a;
|
||||
}
|
||||
|
||||
.accent-mono .link-card-icon {
|
||||
background: var(--tag-bg);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.link-card-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.link-card-title {
|
||||
font-family: 'SF Pro Display', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 4px 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.link-card-desc {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.link-card-arrow {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 13px;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.link-card:hover .link-card-arrow {
|
||||
color: var(--accent);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.links {
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.links-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.link-card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<footer class="page-footer">
|
||||
<div class="container footer-inner">
|
||||
<div class="footer-left">
|
||||
<span class="footer-logo">
|
||||
<span class="logo-bracket"><</span>Madobi Nanami<span class="logo-bracket"> /></span>
|
||||
</span>
|
||||
<span class="footer-copy">© {{ currentYear }} Madobi Nanami Ver.0.0.0.1</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<a
|
||||
v-for="link in socialLinks"
|
||||
:key="link.label"
|
||||
:href="link.url"
|
||||
:aria-label="link.label"
|
||||
:title="link.label"
|
||||
class="social-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i :class="link.icon" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageFooter',
|
||||
setup() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const socialLinks = [
|
||||
{ label: 'Gitea', icon: 'fab fa-github', url: 'https://gitea.nanami.tech/NanamiAdmin' },
|
||||
{ label: 'Email', icon: 'fas fa-envelope', url: 'mailto:contact@nanami.tech' },
|
||||
];
|
||||
|
||||
return {
|
||||
currentYear,
|
||||
socialLinks
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-footer {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 28px 0;
|
||||
background: var(--bg-color);
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
font-family: 'SF Mono', 'Courier New', 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.logo-bracket {
|
||||
color: var(--text-tertiary);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.footer-copy {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
color: var(--text-tertiary);
|
||||
text-decoration: none;
|
||||
font-size: 15px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.social-link:hover {
|
||||
color: var(--accent);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.footer-inner {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div ref="el" class="scroll-reveal" :class="{ 'is-visible': isVisible }" :style="{ transitionDelay: delay + 'ms' }">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'ScrollReveal',
|
||||
props: {
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
threshold: {
|
||||
type: Number,
|
||||
default: 0.15
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const el = ref(null);
|
||||
const isVisible = ref(false);
|
||||
let observer = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (!el.value) return;
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
isVisible.value = true;
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: props.threshold,
|
||||
rootMargin: '0px 0px -40px 0px'
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(el.value);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
el,
|
||||
isVisible
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scroll-reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.scroll-reveal.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<header class="topnav" :class="{ 'is-scrolled': isScrolled, 'menu-open': menuOpen }">
|
||||
<div class="topnav-inner container">
|
||||
<router-link to="/" class="topnav-logo" @click="closeMenu">
|
||||
<span class="logo-bracket"><</span>
|
||||
<span class="logo-text">Madobi Nanami</span>
|
||||
<span class="logo-bracket"> /></span>
|
||||
</router-link>
|
||||
|
||||
<nav class="topnav-links" :class="{ 'is-open': menuOpen }">
|
||||
<router-link
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
class="nav-link"
|
||||
:class="{ active: isActive(item.path) }"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<i :class="item.icon" class="nav-icon" aria-hidden="true"></i>
|
||||
<span>{{ item.label }}</span>
|
||||
</router-link>
|
||||
|
||||
<button class="theme-toggle-mobile" @click="handleToggleTheme" :aria-label="isDarkMode ? '切换到浅色模式' : '切换到深色模式'">
|
||||
<i :class="isDarkMode ? 'fas fa-sun' : 'fas fa-moon'" aria-hidden="true"></i>
|
||||
<span>{{ isDarkMode ? '浅色模式' : '深色模式' }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="topnav-actions">
|
||||
<button class="theme-toggle-btn" @click="toggleTheme" :aria-label="isDarkMode ? '切换到浅色模式' : '切换到深色模式'">
|
||||
<i :class="isDarkMode ? 'fas fa-sun' : 'fas fa-moon'" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="hamburger" :class="{ 'is-active': menuOpen }" @click="toggleMenu" aria-label="切换菜单">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, inject, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
export default {
|
||||
name: 'TopNav',
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const isDarkMode = inject('isDarkMode');
|
||||
const toggleTheme = inject('toggleTheme');
|
||||
const menuOpen = ref(false);
|
||||
const isScrolled = ref(false);
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: '主页', icon: 'fas fa-home' },
|
||||
{ path: '/projects', label: '项目', icon: 'fas fa-code' },
|
||||
{ path: '/contact', label: '联系', icon: 'fas fa-envelope' },
|
||||
{ path: '/about', label: '关于', icon: 'fas fa-user' }
|
||||
];
|
||||
|
||||
const isActive = (path) => {
|
||||
if (path === '/') return route.path === '/';
|
||||
return route.path.startsWith(path);
|
||||
};
|
||||
|
||||
const toggleMenu = () => {
|
||||
menuOpen.value = !menuOpen.value;
|
||||
if (menuOpen.value) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
};
|
||||
|
||||
const closeMenu = () => {
|
||||
menuOpen.value = false;
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
|
||||
const handleToggleTheme = () => {
|
||||
toggleTheme();
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
isScrolled.value = window.scrollY > 10;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
|
||||
return {
|
||||
navItems,
|
||||
isActive,
|
||||
menuOpen,
|
||||
toggleMenu,
|
||||
closeMenu,
|
||||
isScrolled,
|
||||
isDarkMode,
|
||||
toggleTheme,
|
||||
handleToggleTheme
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.topnav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
height: 64px;
|
||||
background: var(--nav-bg);
|
||||
backdrop-filter: var(--nav-blur);
|
||||
-webkit-backdrop-filter: var(--nav-blur);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: background-color 0.3s, border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.topnav.is-scrolled {
|
||||
border-bottom-color: var(--border-color);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.topnav-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.topnav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
text-decoration: none;
|
||||
font-family: 'SF Mono', 'Courier New', 'Consolas', 'Monaco', monospace;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.5px;
|
||||
white-space: nowrap;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.logo-bracket {
|
||||
color: var(--text-tertiary);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: var(--text-color);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.topnav-logo:hover .logo-bracket {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.topnav-logo:hover .logo-text {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Nav Links */
|
||||
.topnav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.29;
|
||||
letter-spacing: -0.224px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--text-color);
|
||||
background: var(--surface-light);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: var(--accent);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 13px;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Theme Toggle (Desktop) */
|
||||
.theme-toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--surface-light);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.theme-toggle-btn:hover {
|
||||
background: var(--surface-hover);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Theme Toggle (Mobile menu) */
|
||||
.theme-toggle-mobile {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-light);
|
||||
color: var(--text-secondary);
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.theme-toggle-mobile:hover {
|
||||
background: var(--surface-hover);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Hamburger */
|
||||
.hamburger {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
z-index: 101;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hamburger span {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 2px;
|
||||
background: var(--text-color);
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.hamburger.is-active span:nth-child(1) {
|
||||
transform: translateY(7px) rotate(45deg);
|
||||
}
|
||||
|
||||
.hamburger.is-active span:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hamburger.is-active span:nth-child(3) {
|
||||
transform: translateY(-7px) rotate(-45deg);
|
||||
}
|
||||
|
||||
.topnav-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.topnav-links {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 80px 24px 40px;
|
||||
background: var(--bg-color);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s, visibility 0.3s;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.topnav-links.is-open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 14px 16px;
|
||||
font-size: 18px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 16px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.theme-toggle-mobile {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router/index.js';
|
||||
import '@fortawesome/fontawesome-free/css/all.css';
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.mount('#app');
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('../views/Home.vue'),
|
||||
meta: { title: 'Nanami — 主页' }
|
||||
},
|
||||
{
|
||||
path: '/projects',
|
||||
name: 'Projects',
|
||||
component: () => import('../views/Projects.vue'),
|
||||
meta: { title: '项目 — Nanami' }
|
||||
},
|
||||
{
|
||||
path: '/contact',
|
||||
name: 'Contact',
|
||||
component: () => import('../views/Contact.vue'),
|
||||
meta: { title: '联系 — Nanami' }
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: () => import('../views/About.vue'),
|
||||
meta: { title: '关于 — Nanami' }
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/'
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
}
|
||||
return { top: 0 };
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
if (to.meta && to.meta.title) {
|
||||
document.title = to.meta.title;
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,241 @@
|
||||
/* ============ Selection ============ */
|
||||
::selection {
|
||||
background: var(--selection-bg);
|
||||
}
|
||||
|
||||
/* ============ Page Header ============ */
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-family: 'SF Pro Display', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: clamp(28px, 4vw, 40px);
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
letter-spacing: normal;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
/* ============ Card ============ */
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: background-color 0.3s, box-shadow 0.3s, transform 0.3s ease;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-family: 'SF Pro Display', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: clamp(18px, 2.5vw, 21px);
|
||||
font-weight: 700;
|
||||
line-height: 1.19;
|
||||
letter-spacing: 0.231px;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ============ Section ============ */
|
||||
.section {
|
||||
padding: 80px 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: 'SF Pro Display', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: clamp(24px, 3.5vw, 32px);
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 12px 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: clamp(15px, 2vw, 17px);
|
||||
line-height: 1.47;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0 0 40px 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
/* ============ Container ============ */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
/* ============ Tag / Badge ============ */
|
||||
.tag {
|
||||
display: inline-block;
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
padding: 4px 12px;
|
||||
border-radius: 980px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.08px;
|
||||
background: var(--tag-bg);
|
||||
color: var(--tag-color);
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
.tag.accent {
|
||||
background: var(--tag-accent-bg);
|
||||
color: var(--tag-accent-color);
|
||||
}
|
||||
|
||||
/* ============ Buttons ============ */
|
||||
.btn {
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 980px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
border: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: var(--surface-light);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* ============ Links ============ */
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* ============ Code / Mono ============ */
|
||||
code, .mono {
|
||||
font-family: 'SF Mono', 'Courier New', 'Consolas', 'Monaco', monospace;
|
||||
background: var(--code-bg);
|
||||
color: var(--code-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* ============ Divider ============ */
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
margin: 0;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
/* ============ Grid Background ============ */
|
||||
.bg-grid {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
opacity: 0.6;
|
||||
background-image:
|
||||
linear-gradient(var(--grid-color) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
transition: background-image 0.3s;
|
||||
}
|
||||
|
||||
/* ============ Scroll Reveal Base ============ */
|
||||
.scroll-reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||
}
|
||||
|
||||
.scroll-reveal.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ============ Empty State ============ */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-tertiary);
|
||||
font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.47;
|
||||
}
|
||||
|
||||
/* ============ Responsive ============ */
|
||||
@media (max-width: 768px) {
|
||||
.section {
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
:root {
|
||||
/* Background */
|
||||
--bg-color: #f8f8f6;
|
||||
--bg-secondary: #f0f0ee;
|
||||
|
||||
/* Text */
|
||||
--text-color: #1a1a1c;
|
||||
--text-secondary: rgba(0, 0, 0, 0.65);
|
||||
--text-tertiary: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* Surface / Card */
|
||||
--card-bg: #ffffff;
|
||||
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 4px 16px rgba(0, 0, 0, 0.04);
|
||||
--card-shadow-hover: 0 2px 8px rgba(0, 0, 0, 0.08), 0 8px 32px rgba(0, 0, 0, 0.06);
|
||||
--surface-light: #f0f0ee;
|
||||
--surface-hover: #e8e8e5;
|
||||
|
||||
/* Accent — Warm Teal */
|
||||
--accent: #2d9d8e;
|
||||
--accent-hover: #238b7d;
|
||||
--accent-subtle: rgba(45, 157, 142, 0.08);
|
||||
--accent-glow: rgba(45, 157, 142, 0.15);
|
||||
|
||||
/* Border */
|
||||
--border-color: rgba(0, 0, 0, 0.06);
|
||||
--border-color-strong: rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Navigation */
|
||||
--nav-bg: rgba(248, 248, 246, 0.8);
|
||||
--nav-blur: saturate(180%) blur(20px);
|
||||
|
||||
/* Avatar ring */
|
||||
--avatar-ring: rgba(45, 157, 142, 0.2);
|
||||
|
||||
/* Tag / Badge */
|
||||
--tag-bg: rgba(0, 0, 0, 0.04);
|
||||
--tag-color: rgba(0, 0, 0, 0.6);
|
||||
--tag-accent-bg: rgba(45, 157, 142, 0.1);
|
||||
--tag-accent-color: #1f7a6e;
|
||||
|
||||
/* Code / Tech elements */
|
||||
--code-bg: rgba(0, 0, 0, 0.03);
|
||||
--code-color: #1a7a6a;
|
||||
|
||||
/* Grid pattern */
|
||||
--grid-color: rgba(0, 0, 0, 0.03);
|
||||
|
||||
/* Focus ring */
|
||||
--focus-ring: rgba(45, 157, 142, 0.4);
|
||||
|
||||
/* Selection */
|
||||
--selection-bg: rgba(45, 157, 142, 0.2);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--bg-color: #0f0f11;
|
||||
--bg-secondary: #16161a;
|
||||
|
||||
--text-color: #e8e8ec;
|
||||
--text-secondary: rgba(255, 255, 255, 0.7);
|
||||
--text-tertiary: rgba(255, 255, 255, 0.4);
|
||||
|
||||
--card-bg: #1a1a1e;
|
||||
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
--card-shadow-hover: 0 2px 8px rgba(0, 0, 0, 0.4), 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
--surface-light: #16161a;
|
||||
--surface-hover: #222228;
|
||||
|
||||
--accent: #4ec9b5;
|
||||
--accent-hover: #5fd4c2;
|
||||
--accent-subtle: rgba(78, 201, 181, 0.1);
|
||||
--accent-glow: rgba(78, 201, 181, 0.15);
|
||||
|
||||
--border-color: rgba(255, 255, 255, 0.06);
|
||||
--border-color-strong: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--nav-bg: rgba(15, 15, 17, 0.8);
|
||||
--nav-blur: saturate(180%) blur(20px);
|
||||
|
||||
--avatar-ring: rgba(78, 201, 181, 0.25);
|
||||
|
||||
--tag-bg: rgba(255, 255, 255, 0.06);
|
||||
--tag-color: rgba(255, 255, 255, 0.6);
|
||||
--tag-accent-bg: rgba(78, 201, 181, 0.12);
|
||||
--tag-accent-color: #4ec9b5;
|
||||
|
||||
--code-bg: rgba(255, 255, 255, 0.04);
|
||||
--code-color: #4ec9b5;
|
||||
|
||||
--grid-color: rgba(255, 255, 255, 0.02);
|
||||
|
||||
--focus-ring: rgba(78, 201, 181, 0.4);
|
||||
|
||||
--selection-bg: rgba(78, 201, 181, 0.25);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
|
||||
export function useTheme() {
|
||||
const isDarkMode = ref(false);
|
||||
|
||||
const updateTheme = () => {
|
||||
if (isDarkMode.value) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
isDarkMode.value = savedTheme === 'dark';
|
||||
} else {
|
||||
isDarkMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
updateTheme();
|
||||
});
|
||||
|
||||
watch(isDarkMode, () => {
|
||||
updateTheme();
|
||||
localStorage.setItem('theme', isDarkMode.value ? 'dark' : 'light');
|
||||
});
|
||||
|
||||
const toggleTheme = () => {
|
||||
isDarkMode.value = !isDarkMode.value;
|
||||
};
|
||||
|
||||
return {
|
||||
isDarkMode,
|
||||
toggleTheme
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="about-page">
|
||||
<AboutSection
|
||||
title="关于我"
|
||||
subtitle="了解我的技术背景、经历与兴趣"
|
||||
/>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Timeline Section -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<ScrollReveal>
|
||||
<h2 class="section-title">技术旅程</h2>
|
||||
<p class="section-subtitle">我的学习与技术成长之路</p>
|
||||
</ScrollReveal>
|
||||
|
||||
<div class="timeline">
|
||||
<ScrollReveal
|
||||
v-for="(item, index) in timeline"
|
||||
:key="item.year"
|
||||
:delay="index * 100"
|
||||
>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-content card">
|
||||
<span class="timeline-year">{{ item.year }}</span>
|
||||
<h3 class="timeline-title">{{ item.title }}</h3>
|
||||
<p class="timeline-desc">{{ item.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Interests Section -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<ScrollReveal>
|
||||
<h2 class="section-title">兴趣领域</h2>
|
||||
<p class="section-subtitle">在工作之外,我热衷于探索这些技术方向</p>
|
||||
</ScrollReveal>
|
||||
|
||||
<div class="interests-grid">
|
||||
<ScrollReveal
|
||||
v-for="interest in interests"
|
||||
:key="interest.title"
|
||||
:delay="100"
|
||||
>
|
||||
<div class="card interest-card">
|
||||
<div class="interest-icon">
|
||||
<i :class="interest.icon" aria-hidden="true"></i>
|
||||
</div>
|
||||
<h3 class="interest-title">{{ interest.title }}</h3>
|
||||
<p class="interest-desc">{{ interest.description }}</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AboutSection from '../components/AboutSection.vue';
|
||||
import ScrollReveal from '../components/ScrollReveal.vue';
|
||||
|
||||
export default {
|
||||
name: 'About',
|
||||
components: {
|
||||
AboutSection,
|
||||
ScrollReveal
|
||||
},
|
||||
setup() {
|
||||
const timeline = [
|
||||
{
|
||||
year: '2013',
|
||||
title: '计算机兴趣启蒙',
|
||||
description: '从老家的一台老旧笔记本电脑开始,对计算机产生了浓厚的兴趣。并开始以此为方向进行自学,探索计算机的各个领域。'
|
||||
},
|
||||
{
|
||||
year: '2017',
|
||||
title: '初次接触开发',
|
||||
description: '以当时较为流行的 "易语言" 作为入门语言,开始学习编程基础,尝试开发简单的桌面应用程序。'
|
||||
},
|
||||
{
|
||||
year: '2018',
|
||||
title: '坚实基础',
|
||||
description: '开始频繁地做计算机桌面运维,不断地积累经验,学习计算机网络、操作系统原理等基础知识,为后续的技术发展打下坚实基础。'
|
||||
},
|
||||
{
|
||||
year: '2022',
|
||||
title: '初识运维',
|
||||
description: '首次了解企业 Linux 服务器运维,开始接触云计算、Docker、Kubernetes 等技术,逐步深入了解现代运维与云原生架构。'
|
||||
},
|
||||
{
|
||||
year: '2024',
|
||||
title: 'HomeLab 运维',
|
||||
description: '搭建了属于自己的 HomeLab,实践企业级服务部署与运维,探索各种技术的边界与可能性。'
|
||||
},
|
||||
{
|
||||
year: 'Future...',
|
||||
title: '持续探索',
|
||||
description: '保持对技术的热爱,持续学习云计算、AI 工程化等前沿技术,分享知识与经验。'
|
||||
}
|
||||
];
|
||||
|
||||
const interests = [
|
||||
{
|
||||
title: '系统编程',
|
||||
description: '深入理解操作系统原理,热衷于用 Go 和 C++ 构建高性能底层软件。',
|
||||
icon: 'fas fa-gears'
|
||||
},
|
||||
{
|
||||
title: '云原生架构',
|
||||
description: '研究 Kubernetes、服务网格与可观测性,构建弹性可扩展的云基础设施。',
|
||||
icon: 'fas fa-cloud'
|
||||
},
|
||||
{
|
||||
title: '开源社区',
|
||||
description: '参与开源项目,相信知识共享与技术协作的力量。',
|
||||
icon: 'fas fa-code-branch'
|
||||
},
|
||||
{
|
||||
title: '网络工程',
|
||||
description: '深入探索网络协议底层知识,构建高效、可靠的网络基础设施。',
|
||||
icon: 'fas fa-network-wired'
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
timeline,
|
||||
interests
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.about-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Timeline */
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 2px;
|
||||
background: var(--border-color);
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
top: 20px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--card-bg);
|
||||
border: 3px solid var(--accent);
|
||||
z-index: 1;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-year {
|
||||
display: inline-block;
|
||||
font-family: 'SF Mono', 'Courier New', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-family: 'SF Pro Display', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 8px 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.timeline-desc {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
/* Interests */
|
||||
.interests-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.interest-card {
|
||||
text-align: center;
|
||||
padding: 28px 20px;
|
||||
}
|
||||
|
||||
.interest-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
font-size: 22px;
|
||||
margin: 0 auto 16px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.interest-card:hover .interest-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.interest-title {
|
||||
font-family: 'SF Pro Display', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 8px 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.interest-desc {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.timeline {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
left: -24px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.timeline::before {
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.interests-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="contact-page">
|
||||
<ContactSection />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContactSection from '../components/ContactSection.vue';
|
||||
|
||||
export default {
|
||||
name: 'Contact',
|
||||
components: {
|
||||
ContactSection
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contact-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<HeroSection />
|
||||
<div class="divider"></div>
|
||||
<LinksSection />
|
||||
<div class="divider"></div>
|
||||
<AboutSection />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeroSection from '../components/HeroSection.vue';
|
||||
import LinksSection from '../components/LinksSection.vue';
|
||||
import AboutSection from '../components/AboutSection.vue';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
HeroSection,
|
||||
LinksSection,
|
||||
AboutSection
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="projects-page">
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<ScrollReveal>
|
||||
<h2 class="section-title">项目作品</h2>
|
||||
<p class="section-subtitle">我参与开发的开源项目与技术实践</p>
|
||||
</ScrollReveal>
|
||||
|
||||
<div class="projects-grid">
|
||||
<ScrollReveal
|
||||
v-for="project in projects"
|
||||
:key="project.name"
|
||||
:delay="100"
|
||||
>
|
||||
<article class="project-card card">
|
||||
<div class="project-header">
|
||||
<div class="project-icon">
|
||||
<i :class="project.icon" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="project-meta">
|
||||
<h3 class="project-name">{{ project.name }}</h3>
|
||||
<span class="project-type">{{ project.type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="project-desc">{{ project.description }}</p>
|
||||
<div class="project-tags">
|
||||
<span v-for="tech in project.tech" :key="tech" class="tag">{{ tech }}</span>
|
||||
</div>
|
||||
<div class="project-links">
|
||||
<a
|
||||
v-if="project.github"
|
||||
:href="project.github"
|
||||
class="btn btn-ghost btn-sm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="fab fa-github"></i> Gitea
|
||||
</a>
|
||||
<a
|
||||
v-if="project.demo"
|
||||
:href="project.demo"
|
||||
class="btn btn-outline btn-sm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="fas fa-external-link-alt"></i> 演示
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScrollReveal from '../components/ScrollReveal.vue';
|
||||
|
||||
export default {
|
||||
name: 'Projects',
|
||||
components: {
|
||||
ScrollReveal
|
||||
},
|
||||
setup() {
|
||||
const projects = [
|
||||
{
|
||||
name: 'Super-frpc',
|
||||
type: '全栈项目',
|
||||
description: 'frpc 实例集成管理系统,提供 Web 管理界面用于管理多个 frp 客户端实例,支持实时日志、状态监控与配置管理。',
|
||||
icon: 'fas fa-network-wired',
|
||||
tech: ['Go', 'Vue 3', 'frp', 'WebSocket', 'Docker'],
|
||||
github: 'https://gitea.nanami.tech/Super-frpc'
|
||||
},
|
||||
{
|
||||
name: 'OtogeCompetitionHelper',
|
||||
type: '全栈项目',
|
||||
description: '音游竞赛辅助工具,为音乐游戏社区提供比赛管理、成绩追踪与统计数据展示的 Web 平台。',
|
||||
icon: 'fas fa-music',
|
||||
tech: ['Go', 'Vue 3', 'WebSocket', 'MySQL', 'Bot API'],
|
||||
github: 'https://gitea.nanami.tech/OtogeCompetitionHelper'
|
||||
},
|
||||
{
|
||||
name: 'NanamiHomePage',
|
||||
type: '前端项目',
|
||||
description: '个人主页项目,使用 Vue 3 构建的纯静态响应式个人主页,支持深色/浅色模式自动切换。',
|
||||
icon: 'fas fa-home',
|
||||
tech: ['Vue 3', 'Vite', 'Vue Router', 'CSS Variables'],
|
||||
github: 'https://gitea.nanami.tech/NanamiWork/HomePage'
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
projects
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.projects-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.project-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
background: var(--accent-subtle);
|
||||
color: var(--accent);
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
font-family: 'SF Pro Display', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 4px 0;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.project-type {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.project-desc {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.project-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.project-links {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user