feat:更新布局样式

This commit is contained in:
2026-04-28 23:09:30 +08:00
parent 7b7150fec9
commit 45d57f7184
18 changed files with 150 additions and 374 deletions

5
Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM nginx:1.27.0
COPY dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' > /etc/timezone

8
nginx.conf Normal file
View File

@@ -0,0 +1,8 @@
server {
listen 80;
server_name _;
# 设置根目录
root /usr/share/nginx/html;
index index.html;
}

View File

@@ -1,10 +1,9 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<!-- 导航栏 --> <navbar />
<navbar :scroll-position="scrollPosition" @scroll-to-section="scrollToSection"></navbar>
<!-- 英雄区域 --> <!-- 英雄区域 -->
<hero-section @scroll-to-section="scrollToSection"></hero-section> <hero-section />
<!-- 技能部分 --> <!-- 技能部分 -->
<skills-section section-id="skills"></skills-section> <skills-section section-id="skills"></skills-section>
@@ -27,7 +26,6 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue'
import Navbar from './components/Navbar.vue' import Navbar from './components/Navbar.vue'
import HeroSection from './components/HeroSection.vue' import HeroSection from './components/HeroSection.vue'
import SkillsSection from './components/SkillsSection.vue' import SkillsSection from './components/SkillsSection.vue'
@@ -36,30 +34,6 @@ import ProjectSection from '@/components/Project/ProjectSection.vue'
import EducationSection from './components/Education/EducationSection.vue' import EducationSection from './components/Education/EducationSection.vue'
import ContactSection from './components/Contact/ContactSection.vue' import ContactSection from './components/Contact/ContactSection.vue'
import Footerbar from './components/Footerbar.vue' import Footerbar from './components/Footerbar.vue'
// 滚动位置
const scrollPosition = ref(0)
// 监听滚动事件
onMounted(() => {
const handleScroll = () => {
scrollPosition.value = window.scrollY
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
})
// 滚动到指定区域
const scrollToSection = (sectionId) => {
const element = document.getElementById(sectionId)
if (element) {
window.scrollTo({
top: element.offsetTop - 80,
behavior: 'smooth'
})
}
}
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -69,6 +43,51 @@ const scrollToSection = (sectionId) => {
.app-container { .app-container {
color: #333; color: #333;
scroll-behavior: smooth; scroll-behavior: smooth;
.light-section {
padding: 8rem 0;
background-color: white;
}
.dark-section {
padding: 8rem 0;
background-color: #f9fafb;
}
.light-card {
background-color: white;
border-radius: 10px;
padding: 2rem;
transition: all 0.3s ease;
&:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
}
}
.dark-card {
background-color: #f9fafb;
border-radius: 10px;
padding: 2rem;
transition: all 0.3s ease;
&:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
}
}
.section-tag {
display: flex;
gap: 10px;
}
.section-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
} }
// 全局动画类 // 全局动画类

View File

@@ -31,22 +31,12 @@
</el-form-item> </el-form-item>
<el-form-item label="消息" prop="message"> <el-form-item label="消息" prop="message">
<el-input <el-input v-model="formData.message" type="textarea" :rows="5" placeholder="请输入您的消息..."/>
v-model="formData.message"
type="textarea"
:rows="5"
placeholder="请输入您的消息..."
></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button <el-button type="primary" size="large" :loading="isSubmitting"
type="primary" @click="submitForm" class="submit-btn">
size="large"
:loading="isSubmitting"
@click="submitForm"
class="submit-btn"
>
<i class="fa fa-paper-plane mr-2"></i> <i class="fa fa-paper-plane mr-2"></i>
<span v-if="!isSubmitting">发送消息</span> <span v-if="!isSubmitting">发送消息</span>
<span v-if="isSubmitting">发送中...</span> <span v-if="isSubmitting">发送中...</span>
@@ -54,11 +44,7 @@
</el-form-item> </el-form-item>
<el-form-item v-if="formSuccess"> <el-form-item v-if="formSuccess">
<el-alert <el-alert title="消息发送成功!感谢您的联系,我会尽快回复您。" type="success" show-icon></el-alert>
title="消息发送成功!感谢您的联系,我会尽快回复您。"
type="success"
show-icon
></el-alert>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>

View File

@@ -6,11 +6,7 @@
</div> </div>
<div class="info-list"> <div class="info-list">
<div <div v-for="(info, index) in profileStore.contactInfo" :key="index" class="info-item">
v-for="(info, index) in profileStore.contactInfo"
:key="index"
class="info-item"
>
<div class="info-icon"> <div class="info-icon">
<i :class="`fa ${info.icon}`"></i> <i :class="`fa ${info.icon}`"></i>
</div> </div>

View File

@@ -1,9 +1,8 @@
<template> <template>
<section id="contact" class="contact-section"> <section id="contact" class="contact-section light-section">
<div class="container"> <div class="section-container">
<div class="section-title animate__animated animate__fadeIn"> <div class="section-title animate__animated animate__fadeIn">
<h2>联系我</h2> <h2>联系我</h2>
<p>如果您对我的技能和经验感兴趣或者有任何合作机会欢迎随时联系我</p>
</div> </div>
<div class="contact-grid"> <div class="contact-grid">
@@ -19,20 +18,11 @@
import ContactInfo from '@/components/Contact/ContactInfo.vue' import ContactInfo from '@/components/Contact/ContactInfo.vue'
import ContactForm from '@/components/Contact/ContactForm.vue' import ContactForm from '@/components/Contact/ContactForm.vue'
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { getObserver } from '@/utils/index.ts'
// 监听滚动,实现元素进入视口时的动画 // 监听滚动,实现元素进入视口时的动画
onMounted(() => { onMounted(() => {
const observer = new IntersectionObserver((entries) => { const observer = getObserver()
entries.forEach((entry) => {
if (entry.isIntersecting) {
const delay = entry.target.getAttribute('data-delay')
setTimeout(() => {
entry.target.classList.add('animate-visible', 'animate__animated', 'animate__fadeInUp')
}, delay ? parseInt(delay) : 0)
observer.unobserve(entry.target)
}
})
}, { threshold: 0.1 })
// 观察联系信息和表单 // 观察联系信息和表单
document.querySelectorAll('.contact-info, .contact-form').forEach((item) => { document.querySelectorAll('.contact-info, .contact-form').forEach((item) => {
@@ -45,14 +35,6 @@ onMounted(() => {
<style lang="scss"> <style lang="scss">
.contact-section { .contact-section {
padding: 1rem 0;
background-color: #FFFFFF;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
.contact-grid { .contact-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -62,8 +44,6 @@ onMounted(() => {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
} }
}
.el-card { .el-card {
height: 100%; height: 100%;
@@ -78,9 +58,8 @@ onMounted(() => {
gap: 10px; gap: 10px;
.card-header { .card-header {
background-color: #f9fafb;
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;
padding: 1rem; padding: 1rem 0;
h3 { h3 {
font-weight: 600; font-weight: 600;
@@ -100,18 +79,13 @@ onMounted(() => {
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-around;
gap: 10px;
margin-bottom: 2rem;
.info-item { .info-item {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 1.5rem; gap: 20px;
&:last-child {
margin-bottom: 0;
}
.info-icon { .info-icon {
width: 40px; width: 40px;
@@ -121,7 +95,6 @@ onMounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 1rem;
flex-shrink: 0; flex-shrink: 0;
i { i {
@@ -132,7 +105,7 @@ onMounted(() => {
.info-content { .info-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 5px;
.info-title { .info-title {
font-weight: 600; font-weight: 600;
@@ -147,43 +120,6 @@ onMounted(() => {
} }
} }
} }
.social-links-section {
border-top: 1px solid #e5e7eb;
padding-top: 1.5rem;
.social-title {
font-weight: 600;
margin-bottom: 1rem;
color: #333;
}
.social-links {
display: flex;
gap: 1rem;
.social-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: white;
color: #666;
text-decoration: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
&:hover {
background-color: #409EFF;
color: white;
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(64, 158, 255, 0.3);
}
}
}
}
} }
.contact-form { .contact-form {

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="education-card animate-item"> <div class="education-card light-card animate-item">
<div class="education-header"> <div class="education-header">
<div class="education-info"> <div class="education-info">
<span class="period">{{ item.period }}</span> <span class="period">{{ item.period }}</span>
<h3 class="degree">{{ item.degree }}</h3> <span class="degree">{{ item.degree }}</span>
<h4 class="school">{{ item.school }}</h4> <span class="school">{{ item.school }}</span>
</div> </div>
<div class="education-icon"> <div class="education-icon">
<i class="fa fa-graduation-cap"></i> <i class="fa fa-graduation-cap"></i>
@@ -15,13 +15,8 @@
<div class="education-details"> <div class="education-details">
<h4 class="details-title">{{ item.detailsTitle }}</h4> <h4 class="details-title">{{ item.detailsTitle }}</h4>
<div class="details-tags"> <div class="section-tag">
<el-tag <el-tag v-for="(detail, i) in item.details" :key="i" type="primary">
v-for="(detail, i) in item.details"
:key="i"
type="warning"
effect="light"
>
{{ detail }} {{ detail }}
</el-tag> </el-tag>
</div> </div>

View File

@@ -1,6 +1,6 @@
<template> <template>
<section id="education" class="education-section"> <section id="education" class="education-section dark-section">
<div class="container"> <div class="section-container">
<div class="section-title animate__animated animate__fadeIn"> <div class="section-title animate__animated animate__fadeIn">
<h2>教育背景</h2> <h2>教育背景</h2>
</div> </div>
@@ -37,15 +37,6 @@ onMounted(() => {
<style lang="scss"> <style lang="scss">
.education-section { .education-section {
padding: 1rem 0;
background-color: #f9fafb;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.education-grid { .education-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -57,16 +48,6 @@ onMounted(() => {
} }
.education-card { .education-card {
background-color: white;
border-radius: 10px;
padding: 2rem;
transition: all 0.3s ease;
&:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
}
.education-header { .education-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -74,25 +55,25 @@ onMounted(() => {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
.education-info { .education-info {
display: flex;
flex-direction: column;
gap: 10px;
.period { .period {
display: inline-block; display: inline-block;
font-size: 0.875rem;
color: #409EFF; color: #409EFF;
font-weight: 500; font-weight: 500;
margin-bottom: 0.5rem;
} }
.degree { .degree {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin: 0 0 0.25rem 0;
} }
.school { .school {
font-size: 1rem; font-size: 1rem;
color: #666; color: #666;
margin: 0;
} }
} }
@@ -115,24 +96,15 @@ onMounted(() => {
.description { .description {
color: #666; color: #666;
line-height: 1.6; line-height: 1.6;
margin-bottom: 1.5rem;
} }
.education-details { .education-details {
border-top: 1px solid #e5e7eb; border-top: 1px solid #e5e7eb;
padding-top: 1.5rem;
.details-title { .details-title {
font-weight: 600; font-weight: 600;
margin-bottom: 1rem;
color: #333; color: #333;
} }
.details-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
} }
} }
} }

View File

@@ -1,16 +1,14 @@
<template> <template>
<div class="timeline-item animate-item"> <div class="timeline-item animate-item">
<!-- 时间点标记 -->
<div class="timeline-dot"> <div class="timeline-dot">
<i class="fa fa-briefcase"></i> <i class="fa fa-briefcase"></i>
</div> </div>
<!-- 内容容器 -->
<div class="timeline-content"> <div class="timeline-content">
<div class="experience-header"> <div class="experience-header">
<span class="period">{{ item.period }}</span> <span class="period">{{ item.period }}</span>
<h3 class="position">{{ item.position }}</h3> <span class="position">{{ item.position }}</span>
<h4 class="company">{{ item.company }}</h4> <span class="company">{{ item.company }}</span>
</div> </div>
<p class="description">{{ item.description }}</p> <p class="description">{{ item.description }}</p>

View File

@@ -1,6 +1,6 @@
<template> <template>
<section id="experience" class="experience-section"> <section id="experience" class="experience-section light-section">
<div class="container"> <div class="section-container">
<div class="section-title animate__animated animate__fadeIn"> <div class="section-title animate__animated animate__fadeIn">
<h2>工作经验</h2> <h2>工作经验</h2>
</div> </div>
@@ -42,18 +42,8 @@ onMounted(() => {
<style lang="scss"> <style lang="scss">
.experience-section { .experience-section {
padding: 1rem 0;
background-color: #FFFFFF;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.timeline { .timeline {
position: relative; position: relative;
margin-top: 3rem;
.timeline-axis { .timeline-axis {
position: absolute; position: absolute;
@@ -143,43 +133,33 @@ onMounted(() => {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
.experience-header { .experience-header {
margin-bottom: 1rem; display: flex;
flex-direction: column;
gap: 10px;
.period { .period {
display: inline-block;
font-size: 0.875rem;
color: #409EFF; color: #409EFF;
font-weight: 500; font-weight: 500;
margin-bottom: 0.5rem;
} }
.position { .position {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
margin-bottom: 0.25rem;
color: #333; color: #333;
} }
.company { .company {
font-size: 1rem; font-size: 1rem;
color: #666; color: #666;
margin: 0;
} }
} }
.description { .description {
color: #666; color: #666;
line-height: 1.6; line-height: 1.6;
margin-bottom: 1rem;
text-align: left; text-align: left;
text-indent: 2em; text-indent: 2em;
} }
.experience-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
} }
} }
} }

View File

@@ -1,16 +1,16 @@
<template> <template>
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="section-container">
<div class="footer-content"> <div class="footer-content">
<div class="footer-brand"> <div class="footer-brand">
<a href="#" class="logo"> <a href="#" class="logo">
Resume<span class="logo-highlight">.</span> Resume<span class="logo-highlight">.</span>
</a> </a>
<p class="brand-description">专业前端工程师的个人简历</p> <p class="brand-description">个人简历</p>
</div> </div>
<div class="footer-copyright"> <div class="footer-copyright">
<p>&copy; {{ new Date().getFullYear() }} {{ profileStore.personalInfo.name }}. 保留所有权利.</p> <p>&copy; {{ new Date().getFullYear() }} {{ profileStore.personalInfo.name }}</p>
<p class="footer-credit">设计与开发 <i class="fa fa-heart"></i></p> <p class="footer-credit">设计与开发 <i class="fa fa-heart"></i></p>
</div> </div>
</div> </div>
@@ -28,13 +28,7 @@ const profileStore = useProfileStore()
.footer { .footer {
background-color: #1d2129; background-color: #1d2129;
color: #86909c; color: #86909c;
padding: 4rem 0 2rem; padding: 2rem 0 2rem;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.footer-content { .footer-content {
display: flex; display: flex;

View File

@@ -31,18 +31,11 @@
<!-- 按钮 --> <!-- 按钮 -->
<div class="action-buttons"> <div class="action-buttons">
<el-button <el-button type="primary" @click="scrollToSection('contact')"
type="primary" class="animate__animated animate__fadeInUp animate__delay-500">
size="large"
@click="$emit('scroll-to-section', 'contact')"
class="animate__animated animate__fadeInUp animate__delay-500"
>
<i class="fa fa-paper-plane mr-2"></i> 联系我 <i class="fa fa-paper-plane mr-2"></i> 联系我
</el-button> </el-button>
<el-button <el-button class="ml-4 animate__animated animate__fadeInUp animate__delay-700">
size="large"
class="ml-4 animate__animated animate__fadeInUp animate__delay-700"
>
<i class="fa fa-download mr-2"></i> 下载简历 <i class="fa fa-download mr-2"></i> 下载简历
</el-button> </el-button>
</div> </div>
@@ -53,13 +46,10 @@
</template> </template>
<script setup> <script setup>
import { getAssetsImageFile } from '@/utils/index.js' import { getAssetsImageFile, scrollToSection } from '@/utils/index.js'
import { useProfileStore } from '@/store/profile.js' import { useProfileStore } from '@/store/profile.js'
const profileStore = useProfileStore() const profileStore = useProfileStore()
// 定义事件
const emits = defineEmits(['scroll-to-section'])
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -71,7 +61,6 @@ const emits = defineEmits(['scroll-to-section'])
.container { .container {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 0 1rem;
} }
.hero-content { .hero-content {

View File

@@ -1,8 +1,5 @@
<template> <template>
<header <header class="navbar">
class="navbar"
:class="{ 'navbar-scrolled': scrollPosition > 50 }"
>
<div class="container"> <div class="container">
<div class="navbar-content"> <div class="navbar-content">
<a href="#" class="logo"> <a href="#" class="logo">
@@ -12,29 +9,22 @@
<!-- 桌面导航 --> <!-- 桌面导航 -->
<nav class="desktop-nav"> <nav class="desktop-nav">
<ul> <ul>
<li><a href="#about" @click.prevent="$emit('scroll-to-section', 'about')">关于我</a></li> <li><a href="#about" @click.prevent="scrollToSection('about')">关于我</a></li>
<li><a href="#skills" @click.prevent="$emit('scroll-to-section', 'skills')">技能</a></li> <li><a href="#skills" @click.prevent="scrollToSection('skills')">技能</a></li>
<li><a href="#education" @click.prevent="$emit('scroll-to-section', 'education')">教育背景</a></li> <li><a href="#education" @click.prevent="scrollToSection('education')">教育背景</a></li>
<li><a href="#experience" @click.prevent="$emit('scroll-to-section', 'experience')">工作经验</a></li> <li><a href="#experience" @click.prevent="scrollToSection('experience')">工作经验</a></li>
<li><a href="#contact" @click.prevent="$emit('scroll-to-section', 'contact')">联系方式</a></li> <li><a href="#contact" @click.prevent="scrollToSection('contact')">联系方式</a></li>
</ul> </ul>
</nav> </nav>
<!-- 移动端菜单按钮 --> <!-- 移动端菜单按钮 -->
<button <el-button type="primary" @click="mobileMenuOpen = !mobileMenuOpen" class="mobile-menu-btn">
class="mobile-menu-btn"
@click="mobileMenuOpen = !mobileMenuOpen"
aria-label="菜单"
>
<i class="fa" :class="mobileMenuOpen ? 'fa-times' : 'fa-bars'"></i> <i class="fa" :class="mobileMenuOpen ? 'fa-times' : 'fa-bars'"></i>
</button> </el-button>
</div> </div>
<!-- 移动端导航菜单 --> <!-- 移动端导航菜单 -->
<div <div class="mobile-nav" :class="{ 'mobile-nav-open': mobileMenuOpen }">
class="mobile-nav"
:class="{ 'mobile-nav-open': mobileMenuOpen }"
>
<ul> <ul>
<li><a href="#about" @click.prevent="handleScroll('about')">关于我</a></li> <li><a href="#about" @click.prevent="handleScroll('about')">关于我</a></li>
<li><a href="#skills" @click.prevent="handleScroll('skills')">技能</a></li> <li><a href="#skills" @click.prevent="handleScroll('skills')">技能</a></li>
@@ -48,15 +38,8 @@
</template> </template>
<script setup> <script setup>
import { ref, defineProps } from 'vue' import { ref } from 'vue'
import { scrollToSection } from '@/utils/index.ts'
// 接收滚动位置属性
const props = defineProps({
scrollPosition: {
type: Number,
default: 0
}
})
// 移动端菜单状态 // 移动端菜单状态
const mobileMenuOpen = ref(false) const mobileMenuOpen = ref(false)
@@ -64,11 +47,8 @@ const mobileMenuOpen = ref(false)
// 处理滚动并关闭菜单 // 处理滚动并关闭菜单
const handleScroll = (sectionId) => { const handleScroll = (sectionId) => {
mobileMenuOpen.value = false mobileMenuOpen.value = false
emits('scroll-to-section', sectionId) scrollToSection(sectionId)
} }
// 定义事件
const emits = defineEmits(['scroll-to-section'])
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -82,22 +62,16 @@ const emits = defineEmits(['scroll-to-section'])
background-color: rgba(255, 255, 255, 0.95); background-color: rgba(255, 255, 255, 0.95);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
&.navbar-scrolled {
padding: 0.5rem 0;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
}
.container { .container {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 0 1rem;
} }
.navbar-content { .navbar-content {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1rem 0; padding: 10px 0;
} }
.logo { .logo {
@@ -105,10 +79,6 @@ const emits = defineEmits(['scroll-to-section'])
font-weight: 700; font-weight: 700;
color: #333; color: #333;
text-decoration: none; text-decoration: none;
.logo-highlight {
color: #409EFF;
}
} }
.desktop-nav { .desktop-nav {
@@ -139,11 +109,6 @@ const emits = defineEmits(['scroll-to-section'])
.mobile-menu-btn { .mobile-menu-btn {
display: none; display: none;
background: none;
border: none;
font-size: 1.5rem;
color: #666;
cursor: pointer;
} }
.mobile-nav { .mobile-nav {

View File

@@ -9,8 +9,8 @@
<div class="timeline-content"> <div class="timeline-content">
<div class="project-header"> <div class="project-header">
<span class="period">{{ props.item.period }}</span> <span class="period">{{ props.item.period }}</span>
<h3 class="name">{{ props.item.name }}</h3> <span class="name">{{ props.item.name }}</span>
<h4 class="role">{{ props.item.role }}</h4> <span class="role">{{ props.item.role }}</span>
</div> </div>
<p class="description">{{ props.item.description }}</p> <p class="description">{{ props.item.description }}</p>
@@ -22,8 +22,8 @@
</ul> </ul>
</div> </div>
<div class="project-tags"> <div class="section-tag">
<el-tag v-for="(tag, index) in props.item.tags" :key="index" effect="light"> <el-tag v-for="(tag, index) in props.item.tags" :key="index">
{{ tag }} {{ tag }}
</el-tag> </el-tag>
</div> </div>

View File

@@ -1,6 +1,6 @@
<template> <template>
<section id="projects" class="projects-section"> <section id="projects" class="projects-section dark-section">
<div class="container"> <div class="section-container">
<div class="section-title animate__animated animate__fadeIn"> <div class="section-title animate__animated animate__fadeIn">
<h2>项目经历</h2> <h2>项目经历</h2>
</div> </div>
@@ -42,15 +42,6 @@ onMounted(() => {
<style lang="scss"> <style lang="scss">
.projects-section { .projects-section {
padding: 1rem 0;
background-color: #f9fafb;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.timeline { .timeline {
position: relative; position: relative;
margin-top: 3rem; margin-top: 3rem;
@@ -137,7 +128,7 @@ onMounted(() => {
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 50%; border-radius: 50%;
background-color: #67C23A; background-color: #409EFF;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -161,27 +152,25 @@ onMounted(() => {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
.project-header { .project-header {
margin-bottom: 1rem; display: flex;
flex-direction: column;
gap: 10px;
.period { .period {
display: inline-block; display: inline-block;
font-size: 0.875rem; color: #409EFF;
color: #67C23A;
font-weight: 500; font-weight: 500;
margin-bottom: 0.5rem;
} }
.name { .name {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
margin-bottom: 0.25rem;
color: #333; color: #333;
} }
.role { .role {
font-size: 1rem; font-size: 1rem;
color: #666; color: #666;
margin: 0;
} }
} }
@@ -211,12 +200,6 @@ onMounted(() => {
} }
} }
} }
.project-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
} }
.project-image { .project-image {

View File

@@ -1,29 +1,20 @@
<template> <template>
<section :id="sectionId" class="skills-section"> <section :id="sectionId" class="skills-section light-section">
<div class="container"> <div class="section-container">
<div class="section-title animate__animated animate__fadeIn"> <div class="section-title animate__animated animate__fadeIn">
<h2>专业技能</h2> <h2>专业技能</h2>
</div> </div>
<div class="skills-grid"> <div class="skills-grid">
<div <div v-for="(skill, index) in profileStore.skills" :key="index"
v-for="(skill, index) in profileStore.skills" class="skill-card dark-card animate-item" :data-delay="index * 150">
:key="index"
class="skill-card animate-item"
:data-delay="index * 150"
>
<div class="skill-icon"> <div class="skill-icon">
<i :class="`fa ${skill.icon}`"></i> <i :class="`fa ${skill.icon}`"></i>
</div> </div>
<h3 class="skill-title">{{ skill.title }}</h3> <h3 class="skill-title">{{ skill.title }}</h3>
<p class="skill-description">{{ skill.description }}</p> <p class="skill-description">{{ skill.description }}</p>
<div class="skill-tags"> <div class="section-tag">
<el-tag <el-tag v-for="(tag, i) in skill.tags" :key="i" type="info">
v-for="(tag, i) in skill.tags"
:key="i"
type="info"
effect="light"
>
{{ tag }} {{ tag }}
</el-tag> </el-tag>
</div> </div>
@@ -36,6 +27,7 @@
<script setup> <script setup>
import { defineProps, onMounted } from 'vue' import { defineProps, onMounted } from 'vue'
import { useProfileStore } from '@/store/profile.ts' import { useProfileStore } from '@/store/profile.ts'
import { getObserver } from '@/utils/index.ts'
const profileStore = useProfileStore() const profileStore = useProfileStore()
@@ -48,22 +40,7 @@ const props = defineProps({
// 监听滚动,实现元素进入视口时的动画,离开时移除动画 // 监听滚动,实现元素进入视口时的动画,离开时移除动画
onMounted(() => { onMounted(() => {
const observer = new IntersectionObserver((entries) => { const observer = getObserver()
entries.forEach((entry) => {
const delay = entry.target.getAttribute('data-delay')
const delayTime = delay ? parseInt(delay) : 0
if (entry.isIntersecting) {
// 元素进入视口,添加动画类
setTimeout(() => {
entry.target.classList.add('animate-visible', 'animate__animated', 'animate__fadeInUp')
}, delayTime)
} else {
// 元素离开视口,移除动画类(以便下次进入时重新触发)
// entry.target.classList.remove('animate-visible', 'animate__animated', 'animate__fadeInUp')
}
})
}, { threshold: 0.1 })
// 观察所有技能卡片 // 观察所有技能卡片
document.querySelectorAll('.skill-card').forEach((card) => { document.querySelectorAll('.skill-card').forEach((card) => {
@@ -76,17 +53,6 @@ onMounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.skills-section { .skills-section {
padding: 8rem 0;
background-color: white;
/* 增加高度让滚动效果更明显 */
min-height: 100vh;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.skills-grid { .skills-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -102,24 +68,6 @@ onMounted(() => {
} }
.skill-card { .skill-card {
background-color: #f9fafb;
border-radius: 10px;
padding: 2rem;
transition: all 0.3s ease;
/* 初始状态设置为不可见 */
opacity: 0;
transform: translateY(20px);
&.animate-visible {
opacity: 1;
transform: translateY(0);
}
&:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
}
.skill-icon { .skill-icon {
width: 60px; width: 60px;
height: 60px; height: 60px;
@@ -128,7 +76,6 @@ onMounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 1.5rem;
i { i {
font-size: 1.5rem; font-size: 1.5rem;
@@ -139,21 +86,13 @@ onMounted(() => {
.skill-title { .skill-title {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
margin-bottom: 1rem;
color: #333; color: #333;
} }
.skill-description { .skill-description {
color: #666; color: #666;
margin-bottom: 1.5rem;
line-height: 1.6; line-height: 1.6;
} }
.skill-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
} }
} }
</style> </style>

View File

@@ -62,7 +62,7 @@ export const useProfileStore = defineStore('profile', {
education: [ education: [
{ {
period: '2015年9月 - 2017年6月', period: '2015年9月 - 2017年6月',
degree: '自动化专业 专科(偏软件方向)', degree: '自动化专业',
school: '南京工业大学', school: '南京工业大学',
description: '主修自动化专业,侧重软件与控制系统开发方向,系统学习数据结构、算法、计算机基础等核心课程,参与多个课程设计与项目实践。', description: '主修自动化专业,侧重软件与控制系统开发方向,系统学习数据结构、算法、计算机基础等核心课程,参与多个课程设计与项目实践。',
detailsTitle: '主要课程', detailsTitle: '主要课程',

View File

@@ -20,3 +20,14 @@ export const getObserver = () => {
}) })
}, { threshold: 0.1 }) }, { threshold: 0.1 })
} }
// 滚动到指定区域
export const scrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId)
if (element) {
window.scrollTo({
top: element.offsetTop - 80,
behavior: 'smooth'
})
}
}