Files
resume/src/components/SkillsSection.vue
2025-10-15 22:41:34 +08:00

161 lines
3.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<section :id="sectionId" class="skills-section">
<div class="container">
<div class="section-title animate__animated animate__fadeIn">
<h2>专业技能</h2>
<p>多年的开发经验使我掌握了多种前端技术和工具能够独立完成从设计到部署的全流程开发工作</p>
</div>
<div class="skills-grid">
<div
v-for="(skill, index) in profileStore.skills"
:key="index"
class="skill-card animate-item"
:data-delay="index * 150"
>
<div class="skill-icon">
<i :class="`fa ${skill.icon}`"></i>
</div>
<h3 class="skill-title">{{ skill.title }}</h3>
<p class="skill-description">{{ skill.description }}</p>
<div class="skill-tags">
<el-tag
v-for="(tag, i) in skill.tags"
:key="i"
type="info"
effect="light"
>
{{ tag }}
</el-tag>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { defineProps, onMounted } from 'vue'
import { useProfileStore } from '@/store/profile.ts'
const profileStore = useProfileStore()
const props = defineProps({
sectionId: {
type: String,
required: true
}
})
// 监听滚动,实现元素进入视口时的动画,离开时移除动画
onMounted(() => {
const observer = new IntersectionObserver((entries) => {
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) => {
observer.observe(card)
})
return () => observer.disconnect()
})
</script>
<style lang="scss" scoped>
.skills-section {
padding: 8rem 0;
background-color: white;
/* 增加高度让滚动效果更明显 */
min-height: 100vh;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.skills-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
@media (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
}
}
.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 {
width: 60px;
height: 60px;
border-radius: 10px;
background-color: rgba(64, 158, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
i {
font-size: 1.5rem;
color: #409EFF;
}
}
.skill-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
color: #333;
}
.skill-description {
color: #666;
margin-bottom: 1.5rem;
line-height: 1.6;
}
.skill-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
}
}
</style>