feat:更新项目经历

This commit is contained in:
2025-11-26 13:21:49 +08:00
parent 796d6ab657
commit 14580f2092
13 changed files with 480 additions and 500 deletions

View File

@@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host 0.0.0.0",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },

793
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -19,26 +19,13 @@
import EducationCard from '@/components/Education/EducationCard.vue' import EducationCard from '@/components/Education/EducationCard.vue'
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { useProfileStore } from '@/store/profile.ts' import { useProfileStore } from '@/store/profile.ts'
import { getObserver } from '@/utils/index.js'
const profileStore = useProfileStore() const profileStore = useProfileStore()
// 监听滚动,实现元素进入视口时的动画 // 监听滚动,实现元素进入视口时的动画
onMounted(() => { onMounted(() => {
const observer = new IntersectionObserver((entries) => { const observer = getObserver()
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__fadeInUp')
}, delayTime)
} else {
// 元素离开视口,移除动画类(以便下次进入时重新触发)
entry.target.classList.remove('animate-visible', 'animate__animated', 'animate__fadeInUp')
}
})
}, { threshold: 0.1 })
// 观察所有教育卡片 // 观察所有教育卡片
document.querySelectorAll('.education-card').forEach((card) => { document.querySelectorAll('.education-card').forEach((card) => {

View File

@@ -29,7 +29,8 @@
<!-- 图片 --> <!-- 图片 -->
<div class="timeline-image"> <div class="timeline-image">
<img :src="item.image" :alt="item.company + '项目图片'" class="experience-image"> <img :src="getAssetsImageFile(item.image)" :alt="item.company + '项目图片'"
class="experience-image">
</div> </div>
</div> </div>
</template> </template>
@@ -37,6 +38,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, PropType } from 'vue' import { defineProps, PropType } from 'vue'
import { IExperience } from '@/types' import { IExperience } from '@/types'
import { getAssetsImageFile } from '@/utils'
const props = defineProps({ const props = defineProps({
item: { item: {

View File

@@ -24,26 +24,13 @@
import ExperienceCard from '@/components/Experience/ExperienceCard.vue' import ExperienceCard from '@/components/Experience/ExperienceCard.vue'
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { useProfileStore } from '@/store/profile.ts' import { useProfileStore } from '@/store/profile.ts'
import { getObserver } from '@/utils/index.js'
const profileStore = useProfileStore() const profileStore = useProfileStore()
// 监听滚动,实现元素进入视口时的动画 // 监听滚动,实现元素进入视口时的动画
onMounted(() => { onMounted(() => {
const observer = new IntersectionObserver((entries) => { const observer = getObserver()
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) => { document.querySelectorAll('.timeline-item').forEach((item) => {

View File

@@ -8,34 +8,34 @@
<!-- 内容容器 --> <!-- 内容容器 -->
<div class="timeline-content"> <div class="timeline-content">
<div class="project-header"> <div class="project-header">
<span class="period">{{ item.period }}</span> <span class="period">{{ props.item.period }}</span>
<h3 class="name">{{ item.name }}</h3> <h3 class="name">{{ props.item.name }}</h3>
<h4 class="role">{{ item.role }}</h4> <h4 class="role">{{ props.item.role }}</h4>
</div> </div>
<p class="description">{{ item.description }}</p> <p class="description">{{ props.item.description }}</p>
<div class="responsibilities" v-if="item.responsibilities && item.responsibilities.length > 0"> <div class="content" v-if="props.item.content && props.item.content.length > 0">
<h4>主要职责</h4> <h4>主要职责</h4>
<ul> <ul>
<li v-for="(resp, i) in item.responsibilities" :key="i">{{ resp }}</li> <li v-for="(resp, i) in props.item.content" :key="i">{{ resp }}</li>
</ul> </ul>
</div> </div>
<div class="project-tags"> <div class="project-tags">
<el-tag <el-tag v-for="(tag, index) in props.item.tags" :key="index" effect="light">
v-for="(tag, index) in item.tags"
:key="index"
effect="light"
>
{{ tag }} {{ tag }}
</el-tag> </el-tag>
</div> </div>
</div> </div>
<!-- 图片 --> <!-- 图片 -->
<div class="timeline-image" v-if="item.image"> <div class="timeline-image" v-if="props.item.images.length > 0">
<img :src="item.image" :alt="item.name + '项目图片'" class="project-image"> <el-carousel height="450px">
<el-carousel-item v-for="(item, index) in props.item.images" :key="index">
<el-image :src="getAssetsImageFile(item)" fit="contain" class="project-image" />
</el-carousel-item>
</el-carousel>
</div> </div>
</div> </div>
</template> </template>
@@ -43,6 +43,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, PropType } from 'vue' import { defineProps, PropType } from 'vue'
import { IProject } from '@/types' import { IProject } from '@/types'
import { getAssetsImageFile } from '@/utils'
const props = defineProps({ const props = defineProps({
item: { item: {

View File

@@ -24,26 +24,13 @@
import ProjectCard from '@/components/Project/ProjectCard.vue' import ProjectCard from '@/components/Project/ProjectCard.vue'
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { useProfileStore } from '@/store/profile.ts' import { useProfileStore } from '@/store/profile.ts'
import { getObserver } from '@/utils/index.js'
const profileStore = useProfileStore() const profileStore = useProfileStore()
// 监听滚动,实现元素进入视口时的动画 // 监听滚动,实现元素进入视口时的动画
onMounted(() => { onMounted(() => {
const observer = new IntersectionObserver((entries) => { const observer = getObserver()
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) => { document.querySelectorAll('.timeline-item').forEach((item) => {
@@ -133,9 +120,10 @@ onMounted(() => {
@media (min-width: 768px) { @media (min-width: 768px) {
display: block; display: block;
position: absolute; position: absolute;
left: -50%; left: -100%;
top: 0; top: 20%;
width: calc(50% - 2rem); // width: calc(50% - 2rem);
width: 100%;
padding-right: 2rem; padding-right: 2rem;
} }
} }
@@ -205,7 +193,7 @@ onMounted(() => {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.responsibilities { .content {
margin-bottom: 1rem; margin-bottom: 1rem;
h4 { h4 {

View File

@@ -61,7 +61,7 @@ onMounted(() => {
}, delayTime) }, delayTime)
} else { } else {
// 元素离开视口,移除动画类(以便下次进入时重新触发) // 元素离开视口,移除动画类(以便下次进入时重新触发)
entry.target.classList.remove('animate-visible', 'animate__animated', 'animate__fadeInUp') // entry.target.classList.remove('animate-visible', 'animate__animated', 'animate__fadeInUp')
} }
}) })
}, { threshold: 0.1 }) }, { threshold: 0.1 })

View File

@@ -66,30 +66,22 @@ export const useProfileStore = defineStore('profile', {
company: '互联网科技有限公司', company: '互联网科技有限公司',
description: '参与多个企业级Web应用的开发负责前端页面实现和交互逻辑编写。与产品和设计团队紧密合作将设计稿转化为高质量的代码解决各种浏览器兼容性问题。', description: '参与多个企业级Web应用的开发负责前端页面实现和交互逻辑编写。与产品和设计团队紧密合作将设计稿转化为高质量的代码解决各种浏览器兼容性问题。',
tags: ['Vue', 'SCSS', 'Webpack'], tags: ['Vue', 'SCSS', 'Webpack'],
image: 'https://picsum.photos/id/180/400/200' image: ''
},
{
period: '2016年6月 - 2017年7月',
position: '前端开发实习生',
company: '数字技术有限公司',
description: '参与公司官网和内部管理系统的开发与维护,学习前端开发技术和最佳实践。协助 senior 工程师完成功能模块开发修复已知bug优化页面性能。',
tags: ['HTML5', 'CSS3', 'jQuery'],
image: 'https://picsum.photos/id/48/400/200'
} }
], ],
education: [ education: [
{ {
period: '2012年9月 - 2016年6月', period: '2015年9月 - 2017年6月',
degree: '计算机科学与技术', degree: '自动化',
school: '北京大学', school: '南京工业大学',
description: '本科期间主修计算机科学与技术专业,系统学习了数据结构、算法、计算机网络等基础知识,参与多个课程设计和科研项目。', description: '本科期间主修计算机科学与技术专业,系统学习了数据结构、算法、计算机网络等基础知识,参与多个课程设计和科研项目。',
detailsTitle: '主要课程', detailsTitle: '主要课程',
details: ['数据结构', '算法分析', '计算机网络', '操作系统', '数据库原理'] details: ['数据结构', '算法分析', '计算机网络', '操作系统', '数据库原理']
}, },
{ {
period: '2016年9月 - 2018年6月', period: '2017年9月 - 2020年6月',
degree: '软件工程', degree: '控制科学与工程',
school: '清华大学', school: '南京工业大学',
description: '研究生阶段专注于软件工程领域研究方向为Web前端技术和用户界面设计发表2篇相关领域的学术论文参与校企合作项目开发。', description: '研究生阶段专注于软件工程领域研究方向为Web前端技术和用户界面设计发表2篇相关领域的学术论文参与校企合作项目开发。',
detailsTitle: '主要成果', detailsTitle: '主要成果',
details: ['学术论文2篇', '优秀毕业生', '校企合作项目', '奖学金'] details: ['学术论文2篇', '优秀毕业生', '校企合作项目', '奖学金']
@@ -97,32 +89,35 @@ export const useProfileStore = defineStore('profile', {
], ],
projects: [ projects: [
{ {
name: '企业级电商平台', name: 'Web边缘管理系统',
period: '2022.03 - 2023.06', period: '2024.01 - 2025.11',
role: '前端负责人', role: '全栈开发工程师',
description: '一个完整的B2B电商平台包含商品管理、订单系统、支付系统等功能模块。', description: '一个面向医疗设备领域的SaaS平台核心功能是实现对患者信息、康复报告、治疗排班的集中化管理。',
responsibilities: [ content: [
'负责前端架构设计和核心模块开发', '前端基于 Vue 3 与 TypeScript 进行开发,采用 Element Plus 组件库构建整体用户界面,并使用 ECharts 实现治疗方案、设备状态等数据的可视化图表与大屏展示。',
'带领5人前端团队完成项目开发', '后端使用 Spring Boot 2 框架构建 RESTful API处理核心业务逻辑集成 MyBatis-Plus 作为数据持久层框架。',
'优化前端性能提升页面加载速度40%', '数据库采用 MySQL负责数据库表结构设计、索引优化与核心 SQL 编写,确保患者信息、治疗报告、排班等数据的高效存储与查询。',
'实现响应式设计,适配多种设备' '主导开发了患者信息管理中心、多治疗师协同任务管理、康复报告生成与审批、智能治疗排班四大核心功能模块的前端界面与后端接口。',
'实现基于 RBAC 模型的权限控制系统,前端进行按钮级权限控制,后端完成接口级访问鉴权。',
'使用 Docker 将前后端应用分别容器化,通过 Docker Compose 定义多服务编排,完成项目的一键部署与运维。'
], ],
tags: ['前端', 'Vue3', 'TypeScript', '负责人', '电商'], tags: ['SpringBoot2', 'Vue3', 'TypeScript', 'ECharts', '电商'],
image: '/images/projects/ecommerce.jpg' images: []
}, },
{ {
name: '智能数据分析系统', name: '健康安全监护物联网平台',
period: '2021.07 - 2022.02', period: '2025.07 - 2025.09',
role: '全栈开发工程师', role: '后端开发工程师 / IoT架构核心成员',
description: '为企业提供数据分析和可视化展示的系统,支持大数据处理和实时分析。', description: '一个基于云-边协同架构的医疗级实时监护平台。项目整合智能手表、毫米波雷达、摄像头等设备通过MQTT、TCP、HTTP等多协议接入实现对生命体征与安全行为的实时采集、传输、存储与Web/移动端可视化告警。',
responsibilities: [ content: [
'参与前后端功能开发', '主导云端通信枢纽架构使用Python构建高并发TCP服务器与HTTP服务器可靠接收智能手表、呼叫器等设备的生命体征与告警数据并统一转发至MQTT消息队列',
'设计并实现数据可视化模块', '负责边缘服务器核心消息处理模块(.NET开发实现MQTT客户端订阅、多源数据心率、血压、异常事件聚合并通过WebSocket服务器向Web/Flutter前端进行毫秒级实时推送',
'优化数据处理算法提升性能30%', '设计并实现时序数据存储方案集成InfluxDB持久化存储海量传感器数据如心率曲线为前端历史查询与分析提供支持使用MySQL管理设备、用户等业务数据',
'编写技术文档和单元测试' '集成RTSP to HLS流媒体服务将视觉相机视频流转换为m3u8格式供Web前端实现低延迟直播监控完成“视频+数据”一体化监护',
'为Web前端与Flutter移动端提供清晰的HTTP RESTful API用于设备管理、指令下发与数据查询确保前后端高效协同'
], ],
tags: ['全栈', 'React', 'Node.js', '大数据', '可视化'], tags: ['物联网', 'Python', '.NET', 'MQTT', 'WebSocket', 'TCP/HTTP', 'InfluxDB', 'MySQL', '云边协同', '实时数据处理'],
image: '/images/projects/analytics.jpg' images: ['IOT消息通讯框架.png', '健康安全监护大屏.png']
} }
], ],
contactInfo: [ contactInfo: [

View File

@@ -21,7 +21,7 @@ export interface IProject {
period: string period: string
role: string role: string
description: string description: string
responsibilities?: string[] content: string[]
tags: string[] tags: string[]
image?: string images: string[]
} }

View File

@@ -1,3 +1,22 @@
export const getAssetsImageFile = (url: string) => { export const getAssetsImageFile = (url: string) => {
return new URL(`/src/assets/${url}`, import.meta.url).href return new URL(`/src/assets/${url}`, import.meta.url).href
} }
export const getObserver = () => {
return new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const classList = ['animate-visible', 'animate__animated', 'animate__fadeInUp']
if (entry.isIntersecting) {
const delay = entry.target.getAttribute('data-delay')
const delayTime = delay ? parseInt(delay, 10) : 0
setTimeout(() => {
entry.target.classList.add(...classList)
}, delayTime)
} else {
// 元素离开视口,移除动画类(以便下次进入时重新触发)
// entry.target.classList.remove(...classList)
}
})
}, { threshold: 0.1 })
}