feat:重构模块架构
This commit is contained in:
150
src/App.vue
150
src/App.vue
@@ -1,45 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<!-- 导航栏 -->
|
<!-- 导航栏 -->
|
||||||
<Navbar
|
<navbar :scroll-position="scrollPosition" @scroll-to-section="scrollToSection"></navbar>
|
||||||
:scroll-position="scrollPosition"
|
|
||||||
@scroll-to-section="scrollToSection"
|
|
||||||
></Navbar>
|
|
||||||
|
|
||||||
<!-- 英雄区域 -->
|
<!-- 英雄区域 -->
|
||||||
<HeroSection
|
<hero-section @scroll-to-section="scrollToSection"></hero-section>
|
||||||
@scroll-to-section="scrollToSection"
|
|
||||||
></HeroSection>
|
|
||||||
|
|
||||||
<!-- 技能部分 -->
|
<!-- 技能部分 -->
|
||||||
<SkillsSection
|
<skills-section section-id="skills"></skills-section>
|
||||||
:skills="skills"
|
|
||||||
section-id="skills"
|
|
||||||
></SkillsSection>
|
|
||||||
|
|
||||||
<!-- 工作经验 -->
|
|
||||||
<ExperienceSection
|
|
||||||
:experiences="experiences"
|
|
||||||
section-id="experience"
|
|
||||||
></ExperienceSection>
|
|
||||||
|
|
||||||
<!-- 教育背景 -->
|
<!-- 教育背景 -->
|
||||||
<EducationSection
|
<education-section />
|
||||||
:education="education"
|
|
||||||
section-id="education"
|
<!-- 工作经验 -->
|
||||||
></EducationSection>
|
<experience-section />
|
||||||
|
|
||||||
|
<!-- 项目经历 -->
|
||||||
|
<project-section />
|
||||||
|
|
||||||
<!-- 联系方式 -->
|
<!-- 联系方式 -->
|
||||||
<ContactSection
|
<contact-section />
|
||||||
:contact-info="contactInfo"
|
|
||||||
:social-links="profileStore.personalInfo.socials"
|
|
||||||
section-id="contact"
|
|
||||||
></ContactSection>
|
|
||||||
|
|
||||||
<!-- 页脚 -->
|
<!-- 页脚 -->
|
||||||
<Footer
|
<footerbar />
|
||||||
:name="profileStore.personalInfo.name"
|
|
||||||
></Footer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -48,14 +31,11 @@ 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'
|
||||||
import ExperienceSection from './components/ExperienceSection.vue'
|
import ExperienceSection from './components/Experience/ExperienceSection.vue'
|
||||||
import EducationSection from './components/EducationSection.vue'
|
import ProjectSection from '@/components/Project/ProjectSection.vue'
|
||||||
import ContactSection from './components/ContactSection.vue'
|
import EducationSection from './components/Education/EducationSection.vue'
|
||||||
import Footer from './components/Footer.vue'
|
import ContactSection from './components/Contact/ContactSection.vue'
|
||||||
|
import Footerbar from './components/Footerbar.vue'
|
||||||
import { useProfileStore } from '@/store/profile.js'
|
|
||||||
|
|
||||||
const profileStore = useProfileStore()
|
|
||||||
|
|
||||||
// 滚动位置
|
// 滚动位置
|
||||||
const scrollPosition = ref(0)
|
const scrollPosition = ref(0)
|
||||||
@@ -80,102 +60,6 @@ const scrollToSection = (sectionId) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 技能数据
|
|
||||||
const skills = [
|
|
||||||
{
|
|
||||||
icon: 'fa-code',
|
|
||||||
title: '前端开发',
|
|
||||||
description: '精通HTML5、CSS3和JavaScript(ES6+),熟悉响应式设计和跨浏览器兼容性处理。',
|
|
||||||
tags: ['HTML5', 'CSS3', 'JavaScript', 'TypeScript']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'fa-react',
|
|
||||||
title: '框架与库',
|
|
||||||
description: '熟练使用React、Vue等主流前端框架,以及相关的状态管理和路由库。',
|
|
||||||
tags: ['React', 'Vue', 'Redux', 'Vuex']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'fa-paint-brush',
|
|
||||||
title: 'UI/UX设计',
|
|
||||||
description: '具备良好的设计感,熟悉主流UI组件库和设计系统,能够实现高质量的用户界面。',
|
|
||||||
tags: ['Element UI', 'Ant Design', 'Figma', 'SCSS']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'fa-cogs',
|
|
||||||
title: '工程化',
|
|
||||||
description: '熟悉前端工程化工具和流程,能够提高开发效率和代码质量。',
|
|
||||||
tags: ['Webpack', 'Vite', 'Git', 'ESLint']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'fa-server',
|
|
||||||
title: '后端知识',
|
|
||||||
description: '了解Node.js和常用后端技术,能够与后端团队高效协作,实现全栈开发。',
|
|
||||||
tags: ['Node.js', 'Express', 'RESTful API', 'MongoDB']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'fa-globe',
|
|
||||||
title: '其他技能',
|
|
||||||
description: '掌握项目管理和沟通技巧,具备良好的英语能力,能够阅读英文技术文档。',
|
|
||||||
tags: ['敏捷开发', '英语', '团队协作', '问题解决']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 工作经验数据
|
|
||||||
const experiences = [
|
|
||||||
{
|
|
||||||
period: '2020年3月 - 至今',
|
|
||||||
position: '高级前端工程师',
|
|
||||||
company: '科技创新有限公司',
|
|
||||||
description: '负责公司核心产品的前端架构设计和开发,优化前端性能,提升用户体验。主导多个大型项目的前端开发工作,带领5人团队完成产品迭代和维护。',
|
|
||||||
tags: ['React', 'TypeScript', 'Redux'],
|
|
||||||
image: 'https://picsum.photos/id/0/400/200'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: '2017年8月 - 2020年2月',
|
|
||||||
position: '前端开发工程师',
|
|
||||||
company: '互联网科技有限公司',
|
|
||||||
description: '参与多个企业级Web应用的开发,负责前端页面实现和交互逻辑编写。与产品和设计团队紧密合作,将设计稿转化为高质量的代码,解决各种浏览器兼容性问题。',
|
|
||||||
tags: ['Vue', 'SCSS', 'Webpack'],
|
|
||||||
image: 'https://picsum.photos/id/180/400/200'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: '2016年6月 - 2017年7月',
|
|
||||||
position: '前端开发实习生',
|
|
||||||
company: '数字技术有限公司',
|
|
||||||
description: '参与公司官网和内部管理系统的开发与维护,学习前端开发技术和最佳实践。协助 senior 工程师完成功能模块开发,修复已知bug,优化页面性能。',
|
|
||||||
tags: ['HTML5', 'CSS3', 'jQuery'],
|
|
||||||
image: 'https://picsum.photos/id/48/400/200'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 教育背景数据
|
|
||||||
const education = [
|
|
||||||
{
|
|
||||||
period: '2012年9月 - 2016年6月',
|
|
||||||
degree: '计算机科学与技术',
|
|
||||||
school: '北京大学',
|
|
||||||
description: '本科期间主修计算机科学与技术专业,系统学习了数据结构、算法、计算机网络等基础知识,参与多个课程设计和科研项目。',
|
|
||||||
detailsTitle: '主要课程',
|
|
||||||
details: ['数据结构', '算法分析', '计算机网络', '操作系统', '数据库原理']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: '2016年9月 - 2018年6月',
|
|
||||||
degree: '软件工程',
|
|
||||||
school: '清华大学',
|
|
||||||
description: '研究生阶段专注于软件工程领域,研究方向为Web前端技术和用户界面设计,发表2篇相关领域的学术论文,参与校企合作项目开发。',
|
|
||||||
detailsTitle: '主要成果',
|
|
||||||
details: ['学术论文2篇', '优秀毕业生', '校企合作项目', '奖学金']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 联系信息数据
|
|
||||||
const contactInfo = [
|
|
||||||
{ icon: 'fa-map-marker', title: '地址', content: '北京市朝阳区建国路88号' },
|
|
||||||
{ icon: 'fa-phone', title: '电话', content: '138-1234-5678' },
|
|
||||||
{ icon: 'fa-envelope', title: '邮箱', content: 'liming@example.com' },
|
|
||||||
{ icon: 'fa-clock-o', title: '工作时间', content: '周一至周五: 9:00 - 18:00' }
|
|
||||||
]
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
135
src/components/Contact/ContactForm.vue
Normal file
135
src/components/Contact/ContactForm.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<div class="contact-form animate-item" data-delay="300">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header" class="card-header">
|
||||||
|
<h3>发送消息</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="80px"
|
||||||
|
class="message-form"
|
||||||
|
@submit.prevent="submitForm"
|
||||||
|
>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="24" :sm="12">
|
||||||
|
<el-form-item label="姓名" prop="name">
|
||||||
|
<el-input v-model="formData.name" placeholder="您的姓名"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="24" :sm="12">
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="formData.email" placeholder="您的邮箱"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item label="主题" prop="subject">
|
||||||
|
<el-input v-model="formData.subject" placeholder="消息主题"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="消息" prop="message">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.message"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
placeholder="请输入您的消息..."
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
@click="submitForm"
|
||||||
|
class="submit-btn"
|
||||||
|
>
|
||||||
|
<i class="fa fa-paper-plane mr-2"></i>
|
||||||
|
<span v-if="!isSubmitting">发送消息</span>
|
||||||
|
<span v-if="isSubmitting">发送中...</span>
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="formSuccess">
|
||||||
|
<el-alert
|
||||||
|
title="消息发送成功!感谢您的联系,我会尽快回复您。"
|
||||||
|
type="success"
|
||||||
|
show-icon
|
||||||
|
></el-alert>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = reactive({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
subject: '',
|
||||||
|
message: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单规则
|
||||||
|
const formRules = {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入您的姓名', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ required: true, message: '请输入您的邮箱', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
subject: [
|
||||||
|
{ required: true, message: '请输入消息主题', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
message: [
|
||||||
|
{ required: true, message: '请输入您的消息', trigger: 'blur' },
|
||||||
|
{ min: 10, message: '消息长度不能少于10个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单引用
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
// 提交状态
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
const formSuccess = ref(false)
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const submitForm = async () => {
|
||||||
|
// 验证表单
|
||||||
|
const valid = await formRef.value.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
// 模拟提交
|
||||||
|
isSubmitting.value = true
|
||||||
|
formSuccess.value = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟API请求延迟
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||||
|
|
||||||
|
// 提交成功
|
||||||
|
formSuccess.value = true
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
formRef.value.resetFields()
|
||||||
|
|
||||||
|
// 5秒后隐藏成功提示
|
||||||
|
setTimeout(() => {
|
||||||
|
formSuccess.value = false
|
||||||
|
}, 5000)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('提交失败,请稍后再试')
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
31
src/components/Contact/ContactInfo.vue
Normal file
31
src/components/Contact/ContactInfo.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div class="contact-info animate-item" data-delay="100">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header" class="card-header">
|
||||||
|
<h3>联系方式</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-list">
|
||||||
|
<div
|
||||||
|
v-for="(info, index) in profileStore.contactInfo"
|
||||||
|
:key="index"
|
||||||
|
class="info-item"
|
||||||
|
>
|
||||||
|
<div class="info-icon">
|
||||||
|
<i :class="`fa ${info.icon}`"></i>
|
||||||
|
</div>
|
||||||
|
<div class="info-content">
|
||||||
|
<h4 class="info-title">{{ info.title }}</h4>
|
||||||
|
<p class="info-text">{{ info.content }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useProfileStore } from '@/store/profile.ts'
|
||||||
|
|
||||||
|
const profileStore = useProfileStore()
|
||||||
|
</script>
|
||||||
203
src/components/Contact/ContactSection.vue
Normal file
203
src/components/Contact/ContactSection.vue
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<section id="contact" class="contact-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-title animate__animated animate__fadeIn">
|
||||||
|
<h2>联系我</h2>
|
||||||
|
<p>如果您对我的技能和经验感兴趣,或者有任何合作机会,欢迎随时联系我。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contact-grid">
|
||||||
|
<contact-info />
|
||||||
|
|
||||||
|
<contact-form />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ContactInfo from '@/components/Contact/ContactInfo.vue'
|
||||||
|
import ContactForm from '@/components/Contact/ContactForm.vue'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
// 监听滚动,实现元素进入视口时的动画
|
||||||
|
onMounted(() => {
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
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) => {
|
||||||
|
observer.observe(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => observer.disconnect()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.contact-section {
|
||||||
|
padding: 1rem 0;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
.contact-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.el-card {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
.el-card__body {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
.info-list {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgba(64, 158, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.info-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
.message-form {
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 15px rgba(64, 158, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,370 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section :id="sectionId" class="contact-section">
|
|
||||||
<div class="container">
|
|
||||||
<div class="section-title animate__animated animate__fadeIn">
|
|
||||||
<h2>联系我</h2>
|
|
||||||
<p>如果您对我的技能和经验感兴趣,或者有任何合作机会,欢迎随时联系我。</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="contact-grid">
|
|
||||||
<!-- 联系信息 -->
|
|
||||||
<div class="contact-info animate-item" data-delay="100">
|
|
||||||
<el-card>
|
|
||||||
<div slot="header" class="card-header">
|
|
||||||
<h3>联系方式</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-list">
|
|
||||||
<div
|
|
||||||
v-for="(info, index) in contactInfo"
|
|
||||||
:key="index"
|
|
||||||
class="info-item"
|
|
||||||
>
|
|
||||||
<div class="info-icon">
|
|
||||||
<i :class="`fa ${info.icon}`"></i>
|
|
||||||
</div>
|
|
||||||
<div class="info-content">
|
|
||||||
<h4 class="info-title">{{ info.title }}</h4>
|
|
||||||
<p class="info-text">{{ info.content }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="social-links-section">
|
|
||||||
<h4 class="social-title">社交媒体</h4>
|
|
||||||
<div class="social-links">
|
|
||||||
<a
|
|
||||||
v-for="(social, index) in socialLinks"
|
|
||||||
:key="index"
|
|
||||||
:href="social.url"
|
|
||||||
:title="social.name"
|
|
||||||
class="social-link"
|
|
||||||
>
|
|
||||||
<i :class="social.icon"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 联系表单 -->
|
|
||||||
<div class="contact-form animate-item" data-delay="300">
|
|
||||||
<el-card>
|
|
||||||
<div slot="header" class="card-header">
|
|
||||||
<h3>发送消息</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-form
|
|
||||||
:model="formData"
|
|
||||||
:rules="formRules"
|
|
||||||
ref="formRef"
|
|
||||||
label-width="80px"
|
|
||||||
class="message-form"
|
|
||||||
@submit.prevent="submitForm"
|
|
||||||
>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="24" :sm="12">
|
|
||||||
<el-form-item label="姓名" prop="name">
|
|
||||||
<el-input v-model="formData.name" placeholder="您的姓名"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="24" :sm="12">
|
|
||||||
<el-form-item label="邮箱" prop="email">
|
|
||||||
<el-input v-model="formData.email" type="email" placeholder="您的邮箱"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-form-item label="主题" prop="subject">
|
|
||||||
<el-input v-model="formData.subject" placeholder="消息主题"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="消息" prop="message">
|
|
||||||
<el-input
|
|
||||||
v-model="formData.message"
|
|
||||||
type="textarea"
|
|
||||||
:rows="5"
|
|
||||||
placeholder="请输入您的消息..."
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
:loading="isSubmitting"
|
|
||||||
@click="submitForm"
|
|
||||||
class="submit-btn"
|
|
||||||
>
|
|
||||||
<i class="fa fa-paper-plane mr-2"></i>
|
|
||||||
<span v-if="!isSubmitting">发送消息</span>
|
|
||||||
<span v-if="isSubmitting">发送中...</span>
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item v-if="formSuccess">
|
|
||||||
<el-alert
|
|
||||||
title="消息发送成功!感谢您的联系,我会尽快回复您。"
|
|
||||||
type="success"
|
|
||||||
show-icon
|
|
||||||
></el-alert>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { defineProps, onMounted, reactive, ref } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
// 接收联系信息、社交链接和sectionId
|
|
||||||
const props = defineProps({
|
|
||||||
contactInfo: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
socialLinks: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
sectionId: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 表单数据
|
|
||||||
const formData = reactive({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
subject: '',
|
|
||||||
message: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 表单规则
|
|
||||||
const formRules = {
|
|
||||||
name: [
|
|
||||||
{ required: true, message: '请输入您的姓名', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
email: [
|
|
||||||
{ required: true, message: '请输入您的邮箱', trigger: 'blur' },
|
|
||||||
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
subject: [
|
|
||||||
{ required: true, message: '请输入消息主题', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
message: [
|
|
||||||
{ required: true, message: '请输入您的消息', trigger: 'blur' },
|
|
||||||
{ min: 10, message: '消息长度不能少于10个字符', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 表单引用
|
|
||||||
const formRef = ref(null)
|
|
||||||
|
|
||||||
// 提交状态
|
|
||||||
const isSubmitting = ref(false)
|
|
||||||
const formSuccess = ref(false)
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const submitForm = async () => {
|
|
||||||
// 验证表单
|
|
||||||
const valid = await formRef.value.validate()
|
|
||||||
if (!valid) return
|
|
||||||
|
|
||||||
// 模拟提交
|
|
||||||
isSubmitting.value = true
|
|
||||||
formSuccess.value = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 模拟API请求延迟
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
|
||||||
|
|
||||||
// 提交成功
|
|
||||||
formSuccess.value = true
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
formRef.value.resetFields()
|
|
||||||
|
|
||||||
// 5秒后隐藏成功提示
|
|
||||||
setTimeout(() => {
|
|
||||||
formSuccess.value = false
|
|
||||||
}, 5000)
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('提交失败,请稍后再试')
|
|
||||||
} finally {
|
|
||||||
isSubmitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听滚动,实现元素进入视口时的动画
|
|
||||||
onMounted(() => {
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
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) => {
|
|
||||||
observer.observe(item)
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => observer.disconnect()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.contact-section {
|
|
||||||
padding: 8rem 0;
|
|
||||||
background-color: #f9fafb;
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-card {
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
background-color: #f9fafb;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-card__body {
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-info {
|
|
||||||
.info-list {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: rgba(64, 158, 255, 0.1);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 1rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
i {
|
|
||||||
color: #409EFF;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-content {
|
|
||||||
.info-title {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-text {
|
|
||||||
color: #666;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
|
||||||
.message-form {
|
|
||||||
.submit-btn {
|
|
||||||
width: 100%;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: 0 6px 15px rgba(64, 158, 255, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
42
src/components/Education/EducationCard.vue
Normal file
42
src/components/Education/EducationCard.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="education-card animate-item">
|
||||||
|
<div class="education-header">
|
||||||
|
<div class="education-info">
|
||||||
|
<span class="period">{{ item.period }}</span>
|
||||||
|
<h3 class="degree">{{ item.degree }}</h3>
|
||||||
|
<h4 class="school">{{ item.school }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="education-icon">
|
||||||
|
<i class="fa fa-graduation-cap"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="description">{{ item.description }}</p>
|
||||||
|
|
||||||
|
<div class="education-details">
|
||||||
|
<h4 class="details-title">{{ item.detailsTitle }}:</h4>
|
||||||
|
<div class="details-tags">
|
||||||
|
<el-tag
|
||||||
|
v-for="(detail, i) in item.details"
|
||||||
|
:key="i"
|
||||||
|
type="warning"
|
||||||
|
effect="light"
|
||||||
|
>
|
||||||
|
{{ detail }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, PropType } from 'vue'
|
||||||
|
import { IEducation } from '@/types'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
required: true,
|
||||||
|
type: Object as PropType<IEducation>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section :id="sectionId" class="education-section">
|
<section id="education" class="education-section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-title animate__animated animate__fadeIn">
|
<div class="section-title animate__animated animate__fadeIn">
|
||||||
<h2>教育背景</h2>
|
<h2>教育背景</h2>
|
||||||
@@ -7,58 +7,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="education-grid">
|
<div class="education-grid">
|
||||||
<div
|
<education-card v-for="(edu, index) in profileStore.education"
|
||||||
v-for="(edu, index) in education"
|
:key="index" :item="edu" :data-delay="index * 300"
|
||||||
:key="index"
|
/>
|
||||||
class="education-card animate-item"
|
|
||||||
:data-delay="index * 300"
|
|
||||||
>
|
|
||||||
<div class="education-header">
|
|
||||||
<div class="education-info">
|
|
||||||
<span class="period">{{ edu.period }}</span>
|
|
||||||
<h3 class="degree">{{ edu.degree }}</h3>
|
|
||||||
<h4 class="school">{{ edu.school }}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="education-icon">
|
|
||||||
<i class="fa fa-graduation-cap"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="description">{{ edu.description }}</p>
|
|
||||||
|
|
||||||
<div class="education-details">
|
|
||||||
<h4 class="details-title">{{ edu.detailsTitle }}:</h4>
|
|
||||||
<div class="details-tags">
|
|
||||||
<el-tag
|
|
||||||
v-for="(detail, i) in edu.details"
|
|
||||||
:key="i"
|
|
||||||
type="warning"
|
|
||||||
effect="light"
|
|
||||||
>
|
|
||||||
{{ detail }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, onMounted } from 'vue'
|
import EducationCard from '@/components/Education/EducationCard.vue'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useProfileStore } from '@/store/profile.ts'
|
||||||
|
|
||||||
// 接收教育数据和sectionId
|
const profileStore = useProfileStore()
|
||||||
const props = defineProps({
|
|
||||||
education: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
sectionId: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听滚动,实现元素进入视口时的动画
|
// 监听滚动,实现元素进入视口时的动画
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -87,10 +49,10 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
.education-section {
|
.education-section {
|
||||||
padding: 8rem 0;
|
padding: 1rem 0;
|
||||||
background-color: white;
|
background-color: #f9fafb;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
@@ -137,7 +99,6 @@ onMounted(() => {
|
|||||||
.degree {
|
.degree {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
margin: 0 0 0.25rem 0;
|
margin: 0 0 0.25rem 0;
|
||||||
}
|
}
|
||||||
47
src/components/Experience/ExperienceCard.vue
Normal file
47
src/components/Experience/ExperienceCard.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="timeline-item animate-item">
|
||||||
|
<!-- 时间点标记 -->
|
||||||
|
<div class="timeline-dot">
|
||||||
|
<i class="fa fa-briefcase"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容容器 -->
|
||||||
|
<div class="timeline-content">
|
||||||
|
<div class="experience-header">
|
||||||
|
<span class="period">{{ item.period }}</span>
|
||||||
|
<h3 class="position">{{ item.position }}</h3>
|
||||||
|
<h4 class="company">{{ item.company }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="description">{{ item.description }}</p>
|
||||||
|
|
||||||
|
<div class="experience-tags">
|
||||||
|
<el-tag
|
||||||
|
v-for="(tag, i) in item.tags"
|
||||||
|
:key="i"
|
||||||
|
type="success"
|
||||||
|
effect="light"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片 -->
|
||||||
|
<div class="timeline-image">
|
||||||
|
<img :src="item.image" :alt="item.company + '项目图片'" class="experience-image">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, PropType } from 'vue'
|
||||||
|
import { IExperience } from '@/types'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
required: true,
|
||||||
|
type: Object as PropType<IExperience>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section :id="sectionId" class="experience-section">
|
<section id="experience" class="experience-section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-title animate__animated animate__fadeIn">
|
<div class="section-title animate__animated animate__fadeIn">
|
||||||
<h2>工作经验</h2>
|
<h2>工作经验</h2>
|
||||||
@@ -10,65 +10,22 @@
|
|||||||
<!-- 时间线中轴线 -->
|
<!-- 时间线中轴线 -->
|
||||||
<div class="timeline-axis"></div>
|
<div class="timeline-axis"></div>
|
||||||
|
|
||||||
<!-- 经验项 -->
|
<experience-card
|
||||||
<div
|
v-for="(exp, index) in profileStore.experiences"
|
||||||
v-for="(exp, index) in experiences"
|
:key="index" :item="exp" :data-delay="index * 300"
|
||||||
:key="index"
|
:class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 === 1 }"
|
||||||
class="timeline-item animate-item"
|
/>
|
||||||
:class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 === 1 }"
|
|
||||||
:data-delay="index * 200"
|
|
||||||
>
|
|
||||||
<!-- 时间点标记 -->
|
|
||||||
<div class="timeline-dot">
|
|
||||||
<i class="fa fa-briefcase"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 内容容器 -->
|
|
||||||
<div class="timeline-content">
|
|
||||||
<div class="experience-header">
|
|
||||||
<span class="period">{{ exp.period }}</span>
|
|
||||||
<h3 class="position">{{ exp.position }}</h3>
|
|
||||||
<h4 class="company">{{ exp.company }}</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="description">{{ exp.description }}</p>
|
|
||||||
|
|
||||||
<div class="experience-tags">
|
|
||||||
<el-tag
|
|
||||||
v-for="(tag, i) in exp.tags"
|
|
||||||
:key="i"
|
|
||||||
type="success"
|
|
||||||
effect="light"
|
|
||||||
>
|
|
||||||
{{ tag }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 图片 -->
|
|
||||||
<div class="timeline-image">
|
|
||||||
<img :src="exp.image" :alt="exp.company + '项目图片'" class="experience-image">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, onMounted } from 'vue'
|
import ExperienceCard from '@/components/Experience/ExperienceCard.vue'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useProfileStore } from '@/store/profile.ts'
|
||||||
|
|
||||||
// 接收经验数据和sectionId
|
const profileStore = useProfileStore()
|
||||||
const props = defineProps({
|
|
||||||
experiences: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
sectionId: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听滚动,实现元素进入视口时的动画
|
// 监听滚动,实现元素进入视口时的动画
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -97,10 +54,10 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
.experience-section {
|
.experience-section {
|
||||||
padding: 8rem 0;
|
padding: 1rem 0;
|
||||||
background-color: #f9fafb;
|
background-color: #FFFFFF;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
@@ -135,6 +92,12 @@ onMounted(() => {
|
|||||||
left: 0;
|
left: 0;
|
||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
right: -10px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-content {
|
.timeline-content {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
@@ -166,6 +129,12 @@ onMounted(() => {
|
|||||||
padding-left: 60px;
|
padding-left: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: -10px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-image {
|
.timeline-image {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@@ -187,9 +156,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeline-dot {
|
.timeline-dot {
|
||||||
position: absolute;
|
|
||||||
right: -10px;
|
|
||||||
top: 0;
|
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -204,10 +170,15 @@ onMounted(() => {
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.timeline-item-right & {
|
//&.timeline-item-left {
|
||||||
left: -10px;
|
// left: -10px;
|
||||||
right: auto;
|
// right: auto;
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
|
//&.timeline-item-right {
|
||||||
|
// left: -10px;
|
||||||
|
// right: auto;
|
||||||
|
//}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
left: 20px;
|
left: 20px;
|
||||||
@@ -8,9 +8,9 @@
|
|||||||
</a>
|
</a>
|
||||||
<p class="brand-description">专业前端工程师的个人简历</p>
|
<p class="brand-description">专业前端工程师的个人简历</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-copyright">
|
<div class="footer-copyright">
|
||||||
<p>© {{ new Date().getFullYear() }} {{ name }}. 保留所有权利.</p>
|
<p>© {{ 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>
|
||||||
@@ -19,15 +19,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps } from 'vue';
|
import { useProfileStore } from '@/store/profile.ts'
|
||||||
|
|
||||||
// 接收姓名属性
|
const profileStore = useProfileStore()
|
||||||
const props = defineProps({
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -35,58 +29,58 @@ const props = defineProps({
|
|||||||
background-color: #1d2129;
|
background-color: #1d2129;
|
||||||
color: #86909c;
|
color: #86909c;
|
||||||
padding: 4rem 0 2rem;
|
padding: 4rem 0 2rem;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-content {
|
.footer-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-brand {
|
.footer-brand {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
.logo-highlight {
|
.logo-highlight {
|
||||||
color: #409EFF;
|
color: #409EFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-description {
|
.brand-description {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-copyright {
|
.footer-copyright {
|
||||||
p {
|
p {
|
||||||
margin: 0 0 0.5rem 0;
|
margin: 0 0 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-credit {
|
.footer-credit {
|
||||||
color: #409EFF;
|
color: #409EFF;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
margin: 0 0.25rem;
|
margin: 0 0.25rem;
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="#about" @click.prevent="$emit('scroll-to-section', 'about')">关于我</a></li>
|
<li><a href="#about" @click.prevent="$emit('scroll-to-section', 'about')">关于我</a></li>
|
||||||
<li><a href="#skills" @click.prevent="$emit('scroll-to-section', 'skills')">技能</a></li>
|
<li><a href="#skills" @click.prevent="$emit('scroll-to-section', 'skills')">技能</a></li>
|
||||||
<li><a href="#experience" @click.prevent="$emit('scroll-to-section', 'experience')">工作经验</a></li>
|
|
||||||
<li><a href="#education" @click.prevent="$emit('scroll-to-section', 'education')">教育背景</a></li>
|
<li><a href="#education" @click.prevent="$emit('scroll-to-section', 'education')">教育背景</a></li>
|
||||||
|
<li><a href="#experience" @click.prevent="$emit('scroll-to-section', 'experience')">工作经验</a></li>
|
||||||
<li><a href="#contact" @click.prevent="$emit('scroll-to-section', 'contact')">联系方式</a></li>
|
<li><a href="#contact" @click.prevent="$emit('scroll-to-section', 'contact')">联系方式</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
53
src/components/Project/ProjectCard.vue
Normal file
53
src/components/Project/ProjectCard.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="timeline-item animate-item">
|
||||||
|
<!-- 时间点标记 -->
|
||||||
|
<div class="timeline-dot">
|
||||||
|
<i class="fa fa-project-diagram"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容容器 -->
|
||||||
|
<div class="timeline-content">
|
||||||
|
<div class="project-header">
|
||||||
|
<span class="period">{{ item.period }}</span>
|
||||||
|
<h3 class="name">{{ item.name }}</h3>
|
||||||
|
<h4 class="role">{{ item.role }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="description">{{ item.description }}</p>
|
||||||
|
|
||||||
|
<div class="responsibilities" v-if="item.responsibilities && item.responsibilities.length > 0">
|
||||||
|
<h4>主要职责</h4>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(resp, i) in item.responsibilities" :key="i">{{ resp }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="project-tags">
|
||||||
|
<el-tag
|
||||||
|
v-for="(tag, index) in item.tags"
|
||||||
|
:key="index"
|
||||||
|
effect="light"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片 -->
|
||||||
|
<div class="timeline-image" v-if="item.image">
|
||||||
|
<img :src="item.image" :alt="item.name + '项目图片'" class="project-image">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, PropType } from 'vue'
|
||||||
|
import { IProject } from '@/types'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
required: true,
|
||||||
|
type: Object as PropType<IProject>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
245
src/components/Project/ProjectSection.vue
Normal file
245
src/components/Project/ProjectSection.vue
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<template>
|
||||||
|
<section id="projects" class="projects-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-title animate__animated animate__fadeIn">
|
||||||
|
<h2>项目经历</h2>
|
||||||
|
<p>参与过的各类项目,展示了我的技术能力和实践经验。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline">
|
||||||
|
<!-- 时间线中轴线 -->
|
||||||
|
<div class="timeline-axis"></div>
|
||||||
|
|
||||||
|
<project-card
|
||||||
|
v-for="(project, index) in profileStore.projects"
|
||||||
|
:key="index" :item="project" :data-delay="index * 300"
|
||||||
|
:class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 === 1 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ProjectCard from '@/components/Project/ProjectCard.vue'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useProfileStore } from '@/store/profile.ts'
|
||||||
|
|
||||||
|
const profileStore = useProfileStore()
|
||||||
|
|
||||||
|
// 监听滚动,实现元素进入视口时的动画
|
||||||
|
onMounted(() => {
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const delay = entry.target.getAttribute('data-delay')
|
||||||
|
const delayTime = delay ? parseInt(delay) : 0
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
entry.target.classList.add('animate-visible', 'animate__animated', 'animate__fadeIn')
|
||||||
|
}, delayTime)
|
||||||
|
} else {
|
||||||
|
// 元素离开视口,移除动画类(以便下次进入时重新触发)
|
||||||
|
entry.target.classList.remove('animate-visible', 'animate__animated', 'animate__fadeIn')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, { threshold: 0.1 })
|
||||||
|
|
||||||
|
// 观察所有项目项
|
||||||
|
document.querySelectorAll('.timeline-item').forEach((item) => {
|
||||||
|
observer.observe(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => observer.disconnect()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.projects-section {
|
||||||
|
padding: 1rem 0;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 3rem;
|
||||||
|
|
||||||
|
.timeline-axis {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
left: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
|
&.timeline-item-left {
|
||||||
|
left: 0;
|
||||||
|
padding-right: 2rem;
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
right: -10px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-image {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: -50%;
|
||||||
|
top: 0;
|
||||||
|
width: calc(50% - 2rem);
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.timeline-item-right {
|
||||||
|
left: 50%;
|
||||||
|
padding-left: 2rem;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
left: 0;
|
||||||
|
padding-left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: -10px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-image {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: -50%;
|
||||||
|
top: 0;
|
||||||
|
width: calc(50% - 2rem);
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 60px;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #67C23A;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 0 0 4px rgba(103, 194, 58, 0.1);
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
left: 20px;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.project-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
.period {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #67C23A;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsibilities {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<div class="skills-grid">
|
<div class="skills-grid">
|
||||||
<div
|
<div
|
||||||
v-for="(skill, index) in skills"
|
v-for="(skill, index) in profileStore.skills"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="skill-card animate-item"
|
class="skill-card animate-item"
|
||||||
:data-delay="index * 150"
|
:data-delay="index * 150"
|
||||||
@@ -36,13 +36,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, onMounted } from 'vue'
|
import { defineProps, onMounted } from 'vue'
|
||||||
|
import { useProfileStore } from '@/store/profile.ts'
|
||||||
|
|
||||||
|
const profileStore = useProfileStore()
|
||||||
|
|
||||||
// 接收技能数据和sectionId
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
skills: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
sectionId: {
|
sectionId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -12,7 +12,124 @@ export const useProfileStore = defineStore('profile', {
|
|||||||
{ name: 'Twitter', icon: 'fa-brands fa-twitter', url: '#' },
|
{ name: 'Twitter', icon: 'fa-brands fa-twitter', url: '#' },
|
||||||
{ name: 'Email', icon: 'fa-solid fa-envelope', url: '#' }
|
{ name: 'Email', icon: 'fa-solid fa-envelope', url: '#' }
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
icon: 'fa-code',
|
||||||
|
title: '前端开发',
|
||||||
|
description: '精通HTML5、CSS3和JavaScript(ES6+),熟悉响应式设计和跨浏览器兼容性处理。',
|
||||||
|
tags: ['HTML5', 'CSS3', 'JavaScript', 'TypeScript']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'fa-react',
|
||||||
|
title: '框架与库',
|
||||||
|
description: '熟练使用React、Vue等主流前端框架,以及相关的状态管理和路由库。',
|
||||||
|
tags: ['React', 'Vue', 'Redux', 'Vuex']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'fa-paint-brush',
|
||||||
|
title: 'UI/UX设计',
|
||||||
|
description: '具备良好的设计感,熟悉主流UI组件库和设计系统,能够实现高质量的用户界面。',
|
||||||
|
tags: ['Element UI', 'Ant Design', 'Figma', 'SCSS']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'fa-cogs',
|
||||||
|
title: '工程化',
|
||||||
|
description: '熟悉前端工程化工具和流程,能够提高开发效率和代码质量。',
|
||||||
|
tags: ['Webpack', 'Vite', 'Git', 'ESLint']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'fa-server',
|
||||||
|
title: '后端知识',
|
||||||
|
description: '了解Node.js和常用后端技术,能够与后端团队高效协作,实现全栈开发。',
|
||||||
|
tags: ['Node.js', 'Express', 'RESTful API', 'MongoDB']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'fa-globe',
|
||||||
|
title: '其他技能',
|
||||||
|
description: '掌握项目管理和沟通技巧,具备良好的英语能力,能够阅读英文技术文档。',
|
||||||
|
tags: ['敏捷开发', '英语', '团队协作', '问题解决']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
experiences: [
|
||||||
|
{
|
||||||
|
period: '2020年3月 - 至今',
|
||||||
|
position: '高级前端工程师',
|
||||||
|
company: '科技创新有限公司',
|
||||||
|
description: '负责公司核心产品的前端架构设计和开发,优化前端性能,提升用户体验。主导多个大型项目的前端开发工作,带领5人团队完成产品迭代和维护。',
|
||||||
|
tags: ['React', 'TypeScript', 'Redux'],
|
||||||
|
image: 'https://picsum.photos/id/0/400/200'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: '2017年8月 - 2020年2月',
|
||||||
|
position: '前端开发工程师',
|
||||||
|
company: '互联网科技有限公司',
|
||||||
|
description: '参与多个企业级Web应用的开发,负责前端页面实现和交互逻辑编写。与产品和设计团队紧密合作,将设计稿转化为高质量的代码,解决各种浏览器兼容性问题。',
|
||||||
|
tags: ['Vue', 'SCSS', 'Webpack'],
|
||||||
|
image: 'https://picsum.photos/id/180/400/200'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: '2016年6月 - 2017年7月',
|
||||||
|
position: '前端开发实习生',
|
||||||
|
company: '数字技术有限公司',
|
||||||
|
description: '参与公司官网和内部管理系统的开发与维护,学习前端开发技术和最佳实践。协助 senior 工程师完成功能模块开发,修复已知bug,优化页面性能。',
|
||||||
|
tags: ['HTML5', 'CSS3', 'jQuery'],
|
||||||
|
image: 'https://picsum.photos/id/48/400/200'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
education: [
|
||||||
|
{
|
||||||
|
period: '2012年9月 - 2016年6月',
|
||||||
|
degree: '计算机科学与技术',
|
||||||
|
school: '北京大学',
|
||||||
|
description: '本科期间主修计算机科学与技术专业,系统学习了数据结构、算法、计算机网络等基础知识,参与多个课程设计和科研项目。',
|
||||||
|
detailsTitle: '主要课程',
|
||||||
|
details: ['数据结构', '算法分析', '计算机网络', '操作系统', '数据库原理']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: '2016年9月 - 2018年6月',
|
||||||
|
degree: '软件工程',
|
||||||
|
school: '清华大学',
|
||||||
|
description: '研究生阶段专注于软件工程领域,研究方向为Web前端技术和用户界面设计,发表2篇相关领域的学术论文,参与校企合作项目开发。',
|
||||||
|
detailsTitle: '主要成果',
|
||||||
|
details: ['学术论文2篇', '优秀毕业生', '校企合作项目', '奖学金']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: '企业级电商平台',
|
||||||
|
period: '2022.03 - 2023.06',
|
||||||
|
role: '前端负责人',
|
||||||
|
description: '一个完整的B2B电商平台,包含商品管理、订单系统、支付系统等功能模块。',
|
||||||
|
responsibilities: [
|
||||||
|
'负责前端架构设计和核心模块开发',
|
||||||
|
'带领5人前端团队完成项目开发',
|
||||||
|
'优化前端性能,提升页面加载速度40%',
|
||||||
|
'实现响应式设计,适配多种设备'
|
||||||
|
],
|
||||||
|
tags: ['前端', 'Vue3', 'TypeScript', '负责人', '电商'],
|
||||||
|
image: '/images/projects/ecommerce.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '智能数据分析系统',
|
||||||
|
period: '2021.07 - 2022.02',
|
||||||
|
role: '全栈开发工程师',
|
||||||
|
description: '为企业提供数据分析和可视化展示的系统,支持大数据处理和实时分析。',
|
||||||
|
responsibilities: [
|
||||||
|
'参与前后端功能开发',
|
||||||
|
'设计并实现数据可视化模块',
|
||||||
|
'优化数据处理算法,提升性能30%',
|
||||||
|
'编写技术文档和单元测试'
|
||||||
|
],
|
||||||
|
tags: ['全栈', 'React', 'Node.js', '大数据', '可视化'],
|
||||||
|
image: '/images/projects/analytics.jpg'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
contactInfo: [
|
||||||
|
{ icon: 'fa-map-marker', title: '地址', content: '南京市溧水区柘塘街道' },
|
||||||
|
{ icon: 'fa-phone', title: '电话', content: '138-1407-9242' },
|
||||||
|
{ icon: 'fa-envelope', title: '邮箱', content: 'njcxx0822@163.com' }
|
||||||
|
]
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
|
|||||||
27
src/types/index.ts
Normal file
27
src/types/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export interface IEducation {
|
||||||
|
period: string;
|
||||||
|
degree: string;
|
||||||
|
school: string;
|
||||||
|
description: string;
|
||||||
|
detailsTitle: string;
|
||||||
|
details: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExperience {
|
||||||
|
period: string;
|
||||||
|
position: string;
|
||||||
|
company: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProject {
|
||||||
|
name: string
|
||||||
|
period: string
|
||||||
|
role: string
|
||||||
|
description: string
|
||||||
|
responsibilities?: string[]
|
||||||
|
tags: string[]
|
||||||
|
image?: string
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user