This commit is contained in:
2025-02-27 17:10:33 +08:00
parent d3a3e1918c
commit d61028a104
20 changed files with 4274 additions and 96 deletions

311
src/components/Contact.tsx Normal file
View File

@@ -0,0 +1,311 @@
import React, { useState } from "react";
import {
Box,
Button,
Container,
Flex,
Heading,
Input,
Stack,
Text,
Textarea,
useToast,
} from "@chakra-ui/react";
import { FaEnvelope, FaPhone, FaMapMarkerAlt } from "react-icons/fa";
import { motion } from "framer-motion";
import axios from "axios";
interface ContactInfoProps {
icon: React.ReactNode;
title: string;
content: string;
}
const ContactInfo = ({ icon, title, content }: ContactInfoProps) => {
return (
<Flex align="center" mb={4}>
<Box color="blue.500" mr={4}>
{icon}
</Box>
<Box>
<Text fontWeight="bold">{title}</Text>
<Text color="gray.600">{content}</Text>
</Box>
</Flex>
);
};
const Contact = () => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [subject, setSubject] = useState("");
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [cooldown, setCooldown] = useState(0);
const toast = useToast();
const API_URL = process.env.REACT_APP_API_URL || "http://localhost:3001";
// 倒计时函数
React.useEffect(() => {
let timer: NodeJS.Timeout | null = null;
if (cooldown > 0) {
timer = setInterval(() => {
setCooldown((prev) => {
if (prev <= 1) {
if (timer) clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => {
if (timer) clearInterval(timer);
};
}, [cooldown]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// 如果在冷却时间内,不允许提交
if (cooldown > 0) {
toast({
title: "请求过于频繁",
description: `请在${cooldown}秒后再试`,
status: "warning",
duration: 3000,
isClosable: true,
position: "top"
});
return;
}
setLoading(true);
setError("");
try {
// 调用后端API发送邮件
const response = await axios.post(`${API_URL}/api/send-email`, {
name,
email,
subject,
message
});
// 显示成功消息
toast({
title: "发送成功",
description: "消息已发送,感谢您的留言,我会尽快回复您!",
status: "success",
duration: 5000,
isClosable: true,
position: "top"
});
// 重置表单
setName("");
setEmail("");
setSubject("");
setMessage("");
} catch (err: any) {
console.error("发送邮件失败:", err);
// 处理速率限制错误
if (err.response?.status === 429) {
const remainingTime = err.response.data.remainingTime || 60;
setCooldown(remainingTime);
toast({
title: "请求过于频繁",
description: `请在${remainingTime}秒后再试`,
status: "warning",
duration: 5000,
isClosable: true,
position: "top"
});
} else {
// 显示其他错误消息
setError(
err.response?.data?.error ||
"发送邮件失败,请稍后再试或直接通过电话联系我"
);
toast({
title: "发送失败",
description: err.response?.data?.error || "发送邮件失败,请稍后再试",
status: "error",
duration: 5000,
isClosable: true,
position: "top"
});
}
} finally {
setLoading(false);
}
};
return (
<Box py={20} id="contact">
<Container maxW="7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Heading
mb={2}
fontSize={{ base: "3xl", md: "4xl" }}
fontWeight="bold"
textAlign="center"
color="gray.700"
>
</Heading>
<Text
fontSize="xl"
textAlign="center"
mb={10}
color="gray.600"
maxW="3xl"
mx="auto"
>
</Text>
<Flex
direction={{ base: "column", md: "row" }}
gap={8}
bg="white"
borderRadius="lg"
overflow="hidden"
boxShadow="lg"
>
<Box
flex={1}
bg="blue.600"
color="white"
p={{ base: 6, md: 8 }}
position="relative"
overflow="hidden"
>
<Box position="relative" zIndex={2}>
<Heading as="h3" size="lg" mb={6}>
</Heading>
<Stack gap={6}>
<ContactInfo
icon={FaEnvelope({ size: 20 })}
title="邮箱"
content="aisi_verify@163.com"
/>
<ContactInfo
icon={FaPhone({ size: 20 })}
title="电话"
content="+86 185 3901 7628"
/>
<ContactInfo
icon={FaMapMarkerAlt({ size: 20 })}
title="地址"
content="北京市海淀区中关村科技园"
/>
</Stack>
<Text mt={10}>
Java后台开发和大数据项目进行深入交流
</Text>
</Box>
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
bg="blue.700"
opacity={0.5}
zIndex={1}
transform="rotate(-5deg) scale(1.2) translateY(10%)"
/>
</Box>
<Box flex={1.5} p={{ base: 6, md: 8 }}>
<form onSubmit={handleSubmit}>
<Stack gap={4}>
<Box>
<Text fontWeight="bold" mb={1}> *</Text>
<Input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="请输入您的姓名"
borderColor="gray.300"
required
/>
</Box>
<Box>
<Text fontWeight="bold" mb={1}> *</Text>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="请输入您的邮箱"
borderColor="gray.300"
required
/>
</Box>
<Box>
<Text fontWeight="bold" mb={1}></Text>
<Input
type="text"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="请输入主题"
borderColor="gray.300"
/>
</Box>
<Box>
<Text fontWeight="bold" mb={1}> *</Text>
<Textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="请输入您的留言内容"
borderColor="gray.300"
height="150px"
required
/>
</Box>
{error && (
<Text color="red.500" fontSize="sm">
{error}
</Text>
)}
{cooldown > 0 && (
<Text color="orange.500" fontSize="sm">
{cooldown}
</Text>
)}
<Button
mt={4}
colorScheme="blue"
type="submit"
isLoading={loading}
loadingText="发送中..."
size="lg"
fontWeight="normal"
w={{ base: "full", md: "auto" }}
isDisabled={cooldown > 0}
>
{cooldown > 0 ? `请等待 ${cooldown}` : (loading ? "发送中..." : "发送消息")}
</Button>
</Stack>
</form>
</Box>
</Flex>
</motion.div>
</Container>
</Box>
);
};
export default Contact;

View File

@@ -0,0 +1,140 @@
import React from "react";
import {
Box,
Container,
Heading,
Text,
Stack,
Flex,
Circle,
} from "@chakra-ui/react";
import { motion } from "framer-motion";
interface ExperienceItemProps {
position: string;
company: string;
period: string;
description: string;
}
const ExperienceItem = ({ position, company, period, description }: ExperienceItemProps) => {
return (
<Box mb={8} position="relative">
<Flex align="center">
<Circle size="40px" bg="blue.500" color="white" mr={4} fontSize="sm" fontWeight="bold">
{period.split("-")[0]}
</Circle>
<Box>
<Heading as="h3" size="md">
{position}
</Heading>
<Text fontWeight="medium" color="blue.500">
{company}
</Text>
<Text fontSize="sm" color="gray.500" mt={1}>
{period}
</Text>
</Box>
</Flex>
<Box mt={4} ml="52px">
<Text color="gray.600">{description}</Text>
</Box>
</Box>
);
};
const Experience = () => {
const experiences = [
{
position: "高级Java架构师",
company: "某科技有限公司",
period: "2020 - 至今",
description:
"负责公司核心业务系统的架构设计和技术选型主导微服务架构重构和云原生改造带领团队完成多个高并发、高可用的分布式系统开发。优化系统性能解决系统扩展性问题将系统响应时间提升40%,支持每日千万级交易量。",
},
{
position: "大数据开发工程师",
company: "某大数据公司",
period: "2018 - 2020",
description:
"负责公司数据处理平台的开发和优化主要使用Hadoop、Spark和Flink技术栈设计实现了实时和离线数据处理流程开发数据ETL过程构建数据仓库为企业提供数据分析支持。成功将数据处理延迟从小时级降低到分钟级。",
},
{
position: "Java高级开发工程师",
company: "某互联网金融公司",
period: "2016 - 2018",
description:
"负责公司支付系统和风控系统的开发维护使用Spring Boot、Spring Cloud微服务技术栈设计实现高并发交易系统优化数据库访问性能开发分布式事务解决方案。系统日均处理交易50万笔峰值TPS达到3000。",
},
{
position: "Java开发工程师",
company: "某软件开发公司",
period: "2014 - 2016",
description:
"参与企业级应用系统开发负责后台服务和API开发使用Spring、MyBatis等框架实现业务逻辑和数据访问层编写单元测试和集成测试参与code review和性能优化工作。",
},
];
return (
<Box py={20} bg="gray.50" id="experience">
<Container maxW="7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Heading
mb={2}
fontSize={{ base: "3xl", md: "4xl" }}
fontWeight="bold"
textAlign="center"
color="gray.700"
>
</Heading>
<Text
fontSize="xl"
textAlign="center"
mb={10}
color="gray.600"
maxW="3xl"
mx="auto"
>
Java后台开发与大数据工程实践经验
</Text>
<Box
position="relative"
maxWidth="800px"
mx="auto"
px={{ base: 4, md: 0 }}
>
<Box
position="absolute"
left="19px"
top="40px"
bottom="20px"
width="2px"
bg="gray.200"
zIndex={1}
></Box>
<Stack gap={0} position="relative" zIndex={2}>
{experiences.map((exp, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<ExperienceItem {...exp} />
</motion.div>
))}
</Stack>
</Box>
</motion.div>
</Container>
</Box>
);
};
export default Experience;

116
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,116 @@
import React from "react";
// 移除图标导入
// import { FaTwitter, FaYoutube, FaGithub, FaLinkedin } from "react-icons/fa";
// import { IconContext } from "react-icons";
const footerStyle = {
background: "#f7fafc",
color: "#4a5568",
marginTop: "40px"
};
const containerStyle = {
maxWidth: "1200px",
margin: "0 auto",
padding: "40px 16px"
};
const gridStyle = {
display: "grid",
gridTemplateColumns: "2fr 1fr 1fr 1fr",
gap: "32px"
};
const stackStyle = {
display: "flex",
flexDirection: "column" as const,
gap: "24px"
};
const headingStyle = {
color: "#3182ce",
fontSize: "16px",
fontWeight: "bold"
};
const textStyle = {
fontSize: "14px"
};
const socialIconsStyle = {
display: "flex",
flexDirection: "row" as const,
gap: "24px"
};
const iconStyle = {
color: "#718096",
fontSize: "20px",
width: "20px",
height: "20px"
};
export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer style={footerStyle}>
<div style={containerStyle}>
<div style={gridStyle}>
<div style={stackStyle}>
<div>
<h3 style={headingStyle}>
Java开发者
</h3>
</div>
<p style={textStyle}>
© {currentYear} Java开发者.
</p>
<div style={socialIconsStyle}>
<a href="https://github.com/yourusername" target="_blank" rel="noopener noreferrer">
<svg style={iconStyle} viewBox="0 0 496 512" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/>
</svg>
</a>
<a href="https://linkedin.com/in/yourusername" target="_blank" rel="noopener noreferrer">
<svg style={iconStyle} viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/>
</svg>
</a>
<a href="https://twitter.com/yourusername" target="_blank" rel="noopener noreferrer">
<svg style={iconStyle} viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"/>
</svg>
</a>
<a href="https://youtube.com/c/yourusername" target="_blank" rel="noopener noreferrer">
<svg style={iconStyle} viewBox="0 0 576 512" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/>
</svg>
</a>
</div>
</div>
<div style={stackStyle}>
<h4 style={headingStyle}></h4>
<a href="/" style={textStyle}></a>
<a href="#skills" style={textStyle}></a>
<a href="#experience" style={textStyle}></a>
<a href="#projects" style={textStyle}></a>
</div>
<div style={stackStyle}>
<h4 style={headingStyle}></h4>
<a href="/blog" style={textStyle}></a>
<a href="#contact" style={textStyle}></a>
<a href="/resources" style={textStyle}></a>
<a href="/books" style={textStyle}></a>
</div>
<div style={stackStyle}>
<h4 style={headingStyle}></h4>
<p style={textStyle}>邮箱: your.email@example.com</p>
<p style={textStyle}>: +86 138 xxxx xxxx</p>
<p style={textStyle}>地址: 中国·</p>
</div>
</div>
</div>
</footer>
);
}

158
src/components/Hero.tsx Normal file
View File

@@ -0,0 +1,158 @@
import React from "react";
import {
Box,
Button,
Container,
Flex,
Heading,
Image,
Stack,
Text,
Link,
} from "@chakra-ui/react";
import { Icon } from "@chakra-ui/icon";
import { TypeAnimation } from 'react-type-animation';
import { FaGithub, FaLinkedin } from 'react-icons/fa';
import { motion } from "framer-motion";
const Hero = () => {
// 由于useColorModeValue在当前版本可能不可用我们使用简单的方式定义颜色
const textColor = "blue.600";
const headingColor = "gray.800";
const descriptionColor = "gray.600";
const hoverBg = "gray.200";
return (
<Container maxW={"7xl"} id="hero">
<Stack
align={"center"}
py={{ base: 20, md: 28 }}
direction={{ base: "column", md: "row" }}
gap={{ base: 8, md: 10 }}
>
<Stack flex={1} gap={{ base: 5, md: 10 }}>
<Heading
lineHeight={1.1}
fontWeight={600}
fontSize={{ base: "3xl", sm: "4xl", lg: "6xl" }}
>
<Text
as={"span"}
position={"relative"}
color={textColor}
>
</Text>
<br />
<Text as={"span"} color={headingColor}>
Java后台与大数据专家
</Text>
</Heading>
<Box>
<TypeAnimation
sequence={[
'专注于分布式系统开发',
1000,
'精通Spring Boot & Cloud',
1000,
'Hadoop & Spark 数据处理',
1000,
'Kafka & Flink 流处理',
1000
]}
wrapper="span"
speed={50}
style={{ fontSize: '2em', display: 'inline-block', color: descriptionColor }}
repeat={Infinity}
/>
</Box>
<Text color={descriptionColor}>
Java后台开发工程师和大数据专家
</Text>
<Stack
gap={{ base: 4, sm: 6 }}
direction={{ base: "column", sm: "row" }}
>
<Button
onClick={() => document.getElementById('contact')?.scrollIntoView({ behavior: 'smooth' })}
rounded={"full"}
size={"lg"}
fontWeight={"normal"}
px={6}
colorScheme={"blue"}
>
</Button>
<Button
onClick={() => document.getElementById('projects')?.scrollIntoView({ behavior: 'smooth' })}
rounded={"full"}
size={"lg"}
fontWeight={"normal"}
px={6}
colorScheme="gray"
>
<Box mr={2}><Icon as={FaGithub as any} /></Box>
</Button>
</Stack>
<Stack direction={"row"} gap={6}>
<Link href="https://github.com/yourusername" target="_blank">
<Button
variant="ghost"
borderRadius="full"
size="lg"
_hover={{ bg: hoverBg }}
>
<Icon as={FaGithub as any} boxSize="28px" />
</Button>
</Link>
<Link href="https://linkedin.com/in/yourusername" target="_blank">
<Button
variant="ghost"
borderRadius="full"
size="lg"
_hover={{ bg: hoverBg }}
>
<Icon as={FaLinkedin as any} boxSize="28px" />
</Button>
</Link>
</Stack>
</Stack>
<Flex
flex={1}
justify={"center"}
align={"center"}
position={"relative"}
w={"full"}
>
<Box
position={"relative"}
height={"400px"}
rounded={"2xl"}
boxShadow={"2xl"}
width={"full"}
overflow={"hidden"}
>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<Image
alt={"Hero Image"}
fit={"cover"}
align={"center"}
w={"100%"}
h={"100%"}
src={"https://images.unsplash.com/photo-1555066931-4365d14bab8c?q=80&w=2070"}
/>
</motion.div>
</Box>
</Flex>
</Stack>
</Container>
);
};
export default Hero;

19
src/components/Layout.tsx Normal file
View File

@@ -0,0 +1,19 @@
import React from "react";
import Navbar from "./Navbar";
import Footer from "./Footer";
interface LayoutProps {
children: React.ReactNode;
}
export default function Layout({ children }: LayoutProps) {
return (
<div>
<Navbar />
<div style={{ minHeight: "100vh", paddingTop: "60px" }}>
{children}
</div>
<Footer />
</div>
);
}

241
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,241 @@
import React, { useState, useEffect } from "react";
const navbarStyle = {
position: "fixed" as const,
top: 0,
width: "100%",
zIndex: 100,
background: "white",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.1)",
borderBottom: "1px solid #eaeaea"
};
const containerStyle = {
maxWidth: "1200px",
margin: "0 auto",
padding: "12px 16px",
display: "flex",
alignItems: "center",
justifyContent: "space-between"
};
const logoStyle = {
fontWeight: "bold",
fontSize: "20px",
color: "#3182ce"
};
const navLinksStyle = {
display: "flex",
gap: "32px"
};
const linkStyle = {
color: "#4a5568",
fontWeight: 500,
textDecoration: "none",
padding: "8px 0",
position: "relative" as const,
transition: "color 0.3s ease"
};
const activeLinkStyle = {
...linkStyle,
color: "#3182ce",
};
const linkHoverStyle = {
color: "#3182ce",
};
const linkAfterStyle = {
content: '""',
position: "absolute" as const,
width: "0%",
height: "2px",
bottom: "0",
left: "50%",
backgroundColor: "#3182ce",
transition: "all 0.3s ease",
transform: "translateX(-50%)"
};
const activeLinkAfterStyle = {
...linkAfterStyle,
width: "100%"
};
const buttonStyle = {
padding: "8px 16px",
backgroundColor: "#3182ce",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
transition: "background-color 0.3s ease",
};
const buttonHoverStyle = {
backgroundColor: "#2c5282",
};
const mobileMenuStyle = {
display: "none",
"@media (max-width: 768px)": {
display: "block"
}
};
const Navbar = () => {
const [activeSection, setActiveSection] = useState("home");
const [hoveredLink, setHoveredLink] = useState<string | null>(null);
const [isButtonHovered, setIsButtonHovered] = useState(false);
// 监听滚动事件,更新当前活动的导航项
useEffect(() => {
const handleScroll = () => {
const scrollPosition = window.scrollY;
// 获取各个部分的位置
const homeSection = document.getElementById("home")?.offsetTop || 0;
const skillsSection = document.getElementById("skills")?.offsetTop || 0;
const experienceSection = document.getElementById("experience")?.offsetTop || 0;
const projectsSection = document.getElementById("projects")?.offsetTop || 0;
const contactSection = document.getElementById("contact")?.offsetTop || 0;
// 设置一个偏移量,使导航项在滚动到相应部分之前就高亮显示
const offset = 100;
// 根据滚动位置更新活动的导航项
if (scrollPosition < skillsSection - offset) {
setActiveSection("home");
} else if (scrollPosition < experienceSection - offset) {
setActiveSection("skills");
} else if (scrollPosition < projectsSection - offset) {
setActiveSection("experience");
} else if (scrollPosition < contactSection - offset) {
setActiveSection("projects");
} else {
setActiveSection("contact");
}
};
window.addEventListener("scroll", handleScroll);
// 组件卸载时移除事件监听
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
// 获取链接样式
const getLinkStyle = (section: string) => {
const isActive = activeSection === section;
const isHovered = hoveredLink === section;
return {
...(isActive ? activeLinkStyle : linkStyle),
...(isHovered ? linkHoverStyle : {}),
"&::after": isActive ? activeLinkAfterStyle : isHovered ? { ...linkAfterStyle, width: "50%" } : linkAfterStyle
};
};
return (
<nav style={navbarStyle}>
<div style={containerStyle}>
<div style={logoStyle}>
Java开发者
</div>
<div style={navLinksStyle}>
<a
href="/"
id="home-link"
style={getLinkStyle("home")}
onMouseEnter={() => setHoveredLink("home")}
onMouseLeave={() => setHoveredLink(null)}
onClick={() => setActiveSection("home")}
>
<div style={activeSection === "home" ? activeLinkAfterStyle : hoveredLink === "home" ? { ...linkAfterStyle, width: "50%" } : linkAfterStyle} />
</a>
<a
href="#skills"
id="skills-link"
style={getLinkStyle("skills")}
onMouseEnter={() => setHoveredLink("skills")}
onMouseLeave={() => setHoveredLink(null)}
onClick={() => setActiveSection("skills")}
>
<div style={activeSection === "skills" ? activeLinkAfterStyle : hoveredLink === "skills" ? { ...linkAfterStyle, width: "50%" } : linkAfterStyle} />
</a>
<a
href="#experience"
id="experience-link"
style={getLinkStyle("experience")}
onMouseEnter={() => setHoveredLink("experience")}
onMouseLeave={() => setHoveredLink(null)}
onClick={() => setActiveSection("experience")}
>
<div style={activeSection === "experience" ? activeLinkAfterStyle : hoveredLink === "experience" ? { ...linkAfterStyle, width: "50%" } : linkAfterStyle} />
</a>
<a
href="#projects"
id="projects-link"
style={getLinkStyle("projects")}
onMouseEnter={() => setHoveredLink("projects")}
onMouseLeave={() => setHoveredLink(null)}
onClick={() => setActiveSection("projects")}
>
<div style={activeSection === "projects" ? activeLinkAfterStyle : hoveredLink === "projects" ? { ...linkAfterStyle, width: "50%" } : linkAfterStyle} />
</a>
<a
href="#contact"
id="contact-link"
style={getLinkStyle("contact")}
onMouseEnter={() => setHoveredLink("contact")}
onMouseLeave={() => setHoveredLink(null)}
onClick={() => setActiveSection("contact")}
>
<div style={activeSection === "contact" ? activeLinkAfterStyle : hoveredLink === "contact" ? { ...linkAfterStyle, width: "50%" } : linkAfterStyle} />
</a>
</div>
<div>
<button
style={{
...buttonStyle,
...(isButtonHovered ? buttonHoverStyle : {})
}}
onMouseEnter={() => setIsButtonHovered(true)}
onMouseLeave={() => setIsButtonHovered(false)}
onClick={() => {
const contactSection = document.getElementById('contact');
if (contactSection) {
contactSection.scrollIntoView({ behavior: 'smooth' });
setActiveSection("contact");
}
}}
>
</button>
</div>
<div style={mobileMenuStyle}>
<button
style={{ background: "transparent", border: "none" }}
onClick={() => {}}
>
</button>
</div>
</div>
</nav>
);
};
export default Navbar;

189
src/components/Projects.tsx Normal file
View File

@@ -0,0 +1,189 @@
import React from "react";
import {
Box,
Button,
Container,
Flex,
Heading,
Image,
SimpleGrid,
Stack,
Text,
Badge,
Link,
} from "@chakra-ui/react";
import { Icon } from "@chakra-ui/icon";
import { motion } from "framer-motion";
import { FaGithub, FaExternalLinkAlt } from "react-icons/fa";
interface ProjectCardProps {
title: string;
description: string;
image: string;
technologies: string[];
github: string | null;
demo: string | null;
}
const ProjectCard = ({ title, description, image, technologies, github, demo }: ProjectCardProps) => {
return (
<Box
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
boxShadow="lg"
bg="white"
_hover={{ transform: "translateY(-5px)", transition: "transform 0.3s ease" }}
>
<Image
src={image}
alt={title}
objectFit="cover"
h="200px"
w="100%"
/>
<Box p={6}>
<Stack gap={4}>
<Heading as="h3" size="md" color="blue.600">
{title}
</Heading>
<Text color="gray.600" fontSize="sm">
{description}
</Text>
<Flex flexWrap="wrap" gap={2}>
{technologies.map((tech, index) => (
<Badge key={index} colorScheme="blue" fontSize="0.8em" px={2} py={1} borderRadius="full">
{tech}
</Badge>
))}
</Flex>
<Flex gap={4}>
{github && (
<Link href={github} target="_blank">
<Button
size="sm"
colorScheme="gray"
variant="outline"
>
<Icon as={FaGithub as any} mr={2} />
</Button>
</Link>
)}
{demo && (
<Link href={demo} target="_blank">
<Button
size="sm"
colorScheme="blue"
>
<Icon as={FaExternalLinkAlt as any} mr={2} />
</Button>
</Link>
)}
</Flex>
</Stack>
</Box>
</Box>
);
};
const Projects = () => {
const projects = [
{
title: "分布式微服务电商平台",
description: "基于Spring Cloud构建的高可用电商系统包含用户、商品、订单、支付等微服务实现了服务注册发现、配置中心、负载均衡、熔断降级等功能。",
image: "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?q=80&w=2070",
technologies: ["Java", "Spring Boot", "Spring Cloud", "MySQL", "Redis", "RabbitMQ"],
github: "#",
demo: "#",
},
{
title: "实时数据分析平台",
description: "基于Kafka和Flink构建的实时数据处理平台支持实时日志收集、指标计算、异常检测和业务监控应用于电商、金融等多个场景。",
image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?q=80&w=2070",
technologies: ["Java", "Flink", "Kafka", "Elasticsearch", "Kibana", "Prometheus"],
github: "#",
demo: "#",
},
{
title: "分布式数据仓库系统",
description: "构建基于Hadoop生态的数据仓库系统实现离线数据的ETL处理、存储和分析支持多维度数据分析和报表生成。",
image: "https://images.unsplash.com/photo-1504384308090-c894fdcc538d?q=80&w=2070",
technologies: ["Hadoop", "Hive", "Spark", "HBase", "Sqoop", "Airflow"],
github: "#",
demo: null,
},
{
title: "容器化微服务DevOps平台",
description: "搭建基于Kubernetes的微服务DevOps平台实现应用的自动构建、部署、监控和运维提高了开发和运维效率。",
image: "https://images.unsplash.com/photo-1667372393119-3d4c48d07fc9?q=80&w=2070",
technologies: ["Docker", "Kubernetes", "Jenkins", "Prometheus", "Grafana", "Istio"],
github: "#",
demo: null,
},
{
title: "分布式搜索引擎",
description: "基于Elasticsearch构建的分布式搜索系统支持全文检索、模糊匹配、数据聚合等功能应用于企业内部知识库和产品搜索。",
image: "https://images.unsplash.com/photo-1483058712412-4245e9b90334?q=80&w=2070",
technologies: ["Java", "Elasticsearch", "Spring Boot", "Redis", "MySQL", "Vue"],
github: "#",
demo: "#",
},
{
title: "机器学习推荐系统",
description: "基于Spark MLlib实现的个性化推荐系统通过协同过滤和内容特征分析为用户提供精准的产品和内容推荐。",
image: "https://images.unsplash.com/photo-1489549132488-d00b7eee80f1?q=80&w=2073",
technologies: ["Spark", "Python", "MLlib", "Kafka", "Spring Boot", "MySQL"],
github: "#",
demo: null,
},
];
return (
<Box py={20} bg="gray.50" id="projects">
<Container maxW="7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Heading
mb={2}
fontSize={{ base: "3xl", md: "4xl" }}
fontWeight="bold"
textAlign="center"
color="gray.700"
>
</Heading>
<Text
fontSize="xl"
textAlign="center"
mb={10}
color="gray.600"
maxW="3xl"
mx="auto"
>
</Text>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} gap={8}>
{projects.map((project, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<ProjectCard {...project} />
</motion.div>
))}
</SimpleGrid>
</motion.div>
</Container>
</Box>
);
};
export default Projects;

167
src/components/Skills.tsx Normal file
View File

@@ -0,0 +1,167 @@
import React from "react";
import {
Box,
Container,
Flex,
Heading,
SimpleGrid,
Text,
} from "@chakra-ui/react";
import { Icon } from "@chakra-ui/icon";
import { FaJava, FaDatabase, FaCloud, FaServer } from "react-icons/fa";
import { motion } from "framer-motion";
interface SkillData {
name: string;
level: number;
}
interface SkillCategoryProps {
title: string;
icon: React.ReactNode;
skills: SkillData[];
}
const SkillCategory = ({ title, icon, skills }: SkillCategoryProps) => {
const bgColor = "gray.50";
const borderColor = "gray.200";
const titleColor = "blue.600";
return (
<Box
borderWidth="1px"
borderRadius="lg"
borderColor={borderColor}
p={6}
bg={bgColor}
boxShadow="md"
_hover={{ transform: "translateY(-5px)", transition: "all 0.3s ease" }}
>
<Flex align="center" mb={4}>
{icon}
<Heading size="md" ml={2} color={titleColor}>
{title}
</Heading>
</Flex>
<Box bg="gray.200" h="1px" w="100%" mb={4}></Box>
{skills.map((skill, index) => (
<Box key={index} mb={4}>
<Flex justify="space-between" mb={1}>
<Text fontWeight="medium">{skill.name}</Text>
<Text fontWeight="medium">{skill.level}%</Text>
</Flex>
<Box
w="100%"
h="8px"
bg="gray.100"
borderRadius="full"
position="relative"
overflow="hidden"
>
<Box
position="absolute"
h="100%"
w={`${skill.level}%`}
bg="blue.500"
borderRadius="full"
/>
</Box>
</Box>
))}
</Box>
);
};
const Skills = () => {
const headingColor = "gray.700";
const textColor = "gray.600";
const javaSkills = [
{ name: "Java Core", level: 95 },
{ name: "Spring Boot", level: 90 },
{ name: "Spring Cloud", level: 85 },
{ name: "Microservices", level: 88 },
{ name: "JPA/Hibernate", level: 85 }
];
const databaseSkills = [
{ name: "MySQL", level: 90 },
{ name: "PostgreSQL", level: 85 },
{ name: "MongoDB", level: 80 },
{ name: "Redis", level: 88 },
{ name: "Oracle", level: 75 }
];
const bigDataSkills = [
{ name: "Hadoop", level: 85 },
{ name: "Spark", level: 80 },
{ name: "Kafka", level: 88 },
{ name: "Flink", level: 78 },
{ name: "Hive", level: 82 }
];
const cloudSkills = [
{ name: "Docker", level: 90 },
{ name: "Kubernetes", level: 82 },
{ name: "AWS", level: 78 },
{ name: "Azure", level: 75 },
{ name: "CI/CD", level: 85 }
];
return (
<Box py={20} id="skills">
<Container maxW="7xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Heading
mb={2}
fontSize={{ base: "3xl", md: "4xl" }}
fontWeight="bold"
textAlign="center"
color={headingColor}
>
</Heading>
<Text
fontSize="xl"
textAlign="center"
mb={10}
color={textColor}
maxW="3xl"
mx="auto"
>
Java开发和大数据处理经验
</Text>
<SimpleGrid columns={{ base: 1, md: 2 }} gap={8}>
<SkillCategory
title="Java后台开发"
icon={<Icon as={FaJava as any} boxSize="24px" color="#5382a1" />}
skills={javaSkills}
/>
<SkillCategory
title="数据库技术"
icon={<Icon as={FaDatabase as any} boxSize="24px" color="#f29111" />}
skills={databaseSkills}
/>
<SkillCategory
title="大数据技术"
icon={<Icon as={FaServer as any} boxSize="24px" color="#e25a1c" />}
skills={bigDataSkills}
/>
<SkillCategory
title="云原生 & DevOps"
icon={<Icon as={FaCloud as any} boxSize="24px" color="#1c9be2" />}
skills={cloudSkills}
/>
</SimpleGrid>
</motion.div>
</Container>
</Box>
);
};
export default Skills;