init
This commit is contained in:
311
src/components/Contact.tsx
Normal file
311
src/components/Contact.tsx
Normal 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;
|
||||
140
src/components/Experience.tsx
Normal file
140
src/components/Experience.tsx
Normal 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
116
src/components/Footer.tsx
Normal 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
158
src/components/Hero.tsx
Normal 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
19
src/components/Layout.tsx
Normal 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
241
src/components/Navbar.tsx
Normal 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
189
src/components/Projects.tsx
Normal 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
167
src/components/Skills.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user