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

6
.gitignore vendored
View File

@@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
/api/node_modules
# testing
/coverage
@@ -13,11 +14,16 @@
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
/api/.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/api/.env

87
api/README.md Normal file
View File

@@ -0,0 +1,87 @@
# 邮件发送API
这是一个简单的Express后端服务用于处理联系表单的邮件发送功能。
## 安装
```bash
cd api
npm install
```
## 配置
编辑 `.env` 文件,填入您的邮箱配置:
```
# 服务器配置
PORT=3001
# 邮件配置
EMAIL_SERVICE=163 # 邮件服务提供商
EMAIL_HOST=smtp.163.com # SMTP服务器地址
EMAIL_PORT=465 # SMTP端口
EMAIL_SECURE=true # 是否使用SSL/TLS
EMAIL_USER=your-email@163.com # 发送邮件的邮箱地址
EMAIL_PASS=your-password-or-app-password # 邮箱密码或授权码
EMAIL_RECIPIENT=recipient@example.com # 接收邮件的邮箱地址(可选默认与EMAIL_USER相同)
```
对于大部分邮箱服务商,您需要生成专门的"应用密码"或"授权码",而不是使用登录密码。例如:
- 对于163邮箱您需要在邮箱设置中开启SMTP服务并获取授权码
- 对于Gmail您需要开启两步验证并生成应用专用密码
## 运行
```bash
# 开发模式(自动重启)
npm run dev
# 生产模式
npm start
```
服务器将在 http://localhost:3001 上运行。
## API端点
### 发送邮件
```
POST /api/send-email
```
请求体示例:
```json
{
"name": "张三",
"email": "zhangsan@example.com",
"subject": "合作咨询",
"message": "我对您的项目很感兴趣,希望能进一步了解。"
}
```
成功响应:
```json
{
"success": true,
"message": "邮件已成功发送"
}
```
### 测试API
```
GET /api/test
```
响应示例:
```json
{
"message": "API服务器运行正常"
}
```

1152
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
api/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "email-api",
"version": "1.0.0",
"description": "邮件发送API",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"nodemailer": "^6.9.7"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

97
api/server.js Normal file
View File

@@ -0,0 +1,97 @@
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const nodemailer = require('nodemailer');
const dotenv = require('dotenv');
// 加载环境变量
dotenv.config();
const app = express();
const port = process.env.PORT || 3001;
// 中间件
app.use(cors());
app.use(bodyParser.json());
// 创建Nodemailer传输器
const transporter = nodemailer.createTransport({
service: process.env.EMAIL_SERVICE || 'smtp', // 邮件服务商,例如:'gmail', 'qq', '163'等
host: process.env.EMAIL_HOST || 'smtp.163.com', // SMTP服务器
port: parseInt(process.env.EMAIL_PORT || '587'),
secure: process.env.EMAIL_SECURE === 'true', // true for 465, false for other ports
auth: {
user: process.env.EMAIL_USER || '', // 发送方邮箱
pass: process.env.EMAIL_PASS || '', // 授权码或密码
},
});
// 存储IP地址最后发送邮件的时间
const lastEmailSentTime = new Map();
// 速率限制中间件 - 60秒内只能发送一封邮件
const rateLimitMiddleware = (req, res, next) => {
const ip = req.ip || req.connection.remoteAddress;
const currentTime = Date.now();
const lastTime = lastEmailSentTime.get(ip) || 0;
const timeDiff = currentTime - lastTime;
// 如果距离上次发送不足60秒则拒绝请求
if (lastTime && timeDiff < 60000) {
const remainingTime = Math.ceil((60000 - timeDiff) / 1000);
return res.status(429).json({
error: `请求过于频繁,请在${remainingTime}秒后再试`,
remainingTime
});
}
// 更新最后发送时间并继续处理请求
lastEmailSentTime.set(ip, currentTime);
next();
};
// 邮件发送接口
app.post('/api/send-email', rateLimitMiddleware, async (req, res) => {
const { name, email, subject, message } = req.body;
if (!name || !email || !message) {
return res.status(400).json({ error: '请填写所有必填字段' });
}
try {
// 邮件选项
const mailOptions = {
from: `"个人网站联系表单" <${process.env.EMAIL_USER}>`,
to: process.env.EMAIL_RECIPIENT || process.env.EMAIL_USER, // 接收方邮箱
subject: subject ? `新的联系表单消息: ${subject}` : '新的联系表单消息',
html: `
<h3>您有一条新的联系表单消息</h3>
<p><strong>姓名:</strong> ${name}</p>
<p><strong>邮箱:</strong> ${email}</p>
<p><strong>主题:</strong> ${subject || '无'}</p>
<p><strong>消息:</strong></p>
<p>${message.replace(/\n/g, '<br>')}</p>
`,
// 回复选项,回复时会发送到表单提交者的邮箱
replyTo: email
};
// 发送邮件
await transporter.sendMail(mailOptions);
return res.status(200).json({ success: true, message: '邮件已成功发送' });
} catch (error) {
console.error('发送邮件时出错:', error);
return res.status(500).json({ error: '发送邮件失败,请稍后再试' });
}
});
// 测试路由
app.get('/api/test', (req, res) => {
res.json({ message: 'API服务器运行正常' });
});
// 启动服务器
app.listen(port, () => {
console.log(`邮件服务API正在端口 ${port} 上运行`);
});

1200
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,17 +3,33 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@chakra-ui/button": "^2.0.0",
"@chakra-ui/color-mode": "^2.0.0",
"@chakra-ui/hooks": "^2.0.0",
"@chakra-ui/icon": "^3.0.0",
"@chakra-ui/icons": "^2.0.0",
"@chakra-ui/layout": "^2.0.0",
"@chakra-ui/react": "^2.8.0",
"@chakra-ui/theme-utils": "^2.0.0",
"@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"axios": "^1.6.2",
"framer-motion": "^10.16.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0",
"react-router-dom": "^6.20.0",
"react-scripts": "5.0.1",
"react-scroll": "^1.9.0",
"react-type-animation": "^3.2.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},

View File

@@ -1,24 +1,23 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Layout from './components/Layout';
import Hero from './components/Hero';
import Skills from './components/Skills';
import Projects from './components/Projects';
import Experience from './components/Experience';
import Contact from './components/Contact';
// 创建一个简单包装组件而不使用ChakraProvider
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
<div style={{ width: '100%', maxWidth: '100vw', overflow: 'hidden' }}>
<Layout>
<Hero />
<Skills />
<Experience />
<Projects />
<Contact />
</Layout>
</div>
);
}

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;

View File

@@ -3,13 +3,26 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { CustomChakraProvider } from './theme/ChakraContext';
// 使用全局变量
declare global {
interface Window {
ChakraProvider: any;
}
}
// 移除未使用的变量
// const ChakraProvider = window.ChakraProvider || React.Fragment;
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
<CustomChakraProvider>
<App />
</CustomChakraProvider>
</React.StrictMode>
);

354
src/pages/Home.tsx Normal file
View File

@@ -0,0 +1,354 @@
import React from "react";
import {
Box,
Heading,
Container,
Text,
Flex,
Image,
VStack,
HStack,
Badge
} from "@chakra-ui/react";
import { Button } from "@chakra-ui/button";
import { Stack, SimpleGrid } from "@chakra-ui/layout";
import { Icon } from "@chakra-ui/icon";
import { useColorModeValue } from "@chakra-ui/color-mode";
import { createIcon } from "@chakra-ui/icons";
import { Link as RouterLink } from "react-router-dom";
import { TypeAnimation } from "react-type-animation";
import { FaJava, FaDatabase, FaCloud, FaGithub } from "react-icons/fa";
import { SiApachekafka, SiSpring, SiApachehadoop, SiApachespark } from "react-icons/si";
import type { IconType } from "react-icons";
export default function Home() {
return (
<Box>
{/* Hero Section */}
<Container maxW={"3xl"}>
<Stack
as={Box}
textAlign={"center"}
gap={{ base: 8, md: 14 }}
py={{ base: 20, md: 36 }}
>
<Heading
fontWeight={700}
fontSize={{ base: "2xl", sm: "4xl", md: "6xl" }}
lineHeight={"110%"}
>
<Text as={"span"} color={"brand.500"}>
Java全栈开发者
</Text>
<br />
<Text as={"span"} color={"accent.500"} fontSize={{ base: "xl", sm: "2xl", md: "4xl" }}>
<TypeAnimation
sequence={[
"专注于后台开发", 2000,
"热爱大数据技术", 2000,
"架构设计师", 2000,
"问题解决者", 2000
]}
wrapper="span"
cursor={true}
repeat={Infinity}
/>
</Text>
</Heading>
<Text color={"gray.500"} fontSize={{ base: "lg", md: "xl" }}>
Java开发者
</Text>
<Stack
direction={"column"}
gap={3}
align={"center"}
alignSelf={"center"}
position={"relative"}
>
<Button
colorScheme={"brand"}
bg={"brand.500"}
rounded={"full"}
px={6}
_hover={{
bg: "brand.600",
}}
onClick={() => window.location.href = '/projects'}
>
</Button>
<Button
variant={"outline"}
colorScheme={"brand"}
rounded={"full"}
px={6}
onClick={() => window.location.href = '/contact'}
>
</Button>
<Box>
<Icon
as={Arrow}
color={useColorModeValue("gray.800", "gray.300")}
w={71}
position={"absolute"}
right={-71}
top={"10px"}
/>
<Text
fontSize={"lg"}
fontFamily={"Caveat"}
position={"absolute"}
right={"-125px"}
top={"-15px"}
transform={"rotate(10deg)"}
>
</Text>
</Box>
</Stack>
</Stack>
</Container>
{/* Skills Overview */}
<Box bg={useColorModeValue("gray.50", "gray.900")} py={20}>
<Container maxW={"6xl"}>
<Heading
textAlign={"center"}
fontSize={"4xl"}
py={10}
fontWeight={"bold"}
>
</Heading>
<SimpleGrid columns={{ base: 2, md: 4 }} gap={10}>
<SkillCard
icon={FaJava}
title="Java开发"
description="精通Java核心技术Spring生态系统和JVM优化"
/>
<SkillCard
icon={SiApachekafka}
title="消息队列"
description="Kafka、RabbitMQ等消息中间件的设计和优化"
/>
<SkillCard
icon={FaDatabase}
title="数据库"
description="关系型数据库与NoSQL数据库的设计和优化"
/>
<SkillCard
icon={SiApachehadoop}
title="大数据技术"
description="Hadoop生态系统包括HDFS、MapReduce等"
/>
<SkillCard
icon={SiApachespark}
title="Spark"
description="使用Spark进行大规模数据处理和分析"
/>
<SkillCard
icon={SiSpring}
title="Spring生态"
description="Spring Boot、Spring Cloud微服务架构"
/>
<SkillCard
icon={FaCloud}
title="云原生"
description="Docker、Kubernetes容器化技术与部署"
/>
<SkillCard
icon={FaGithub}
title="版本控制"
description="Git、Maven、Gradle等开发工具"
/>
</SimpleGrid>
</Container>
</Box>
{/* Featured Projects */}
<Box py={20}>
<Container maxW={"6xl"}>
<Heading
textAlign={"center"}
fontSize={"4xl"}
py={10}
fontWeight={"bold"}
>
</Heading>
<SimpleGrid columns={{ base: 1, md: 2 }} gap={10}>
<ProjectCard
title="分布式电商系统"
description="基于Spring Cloud的微服务架构实现了高可用、高并发的电商平台。"
tags={["Spring Cloud", "微服务", "MySQL", "Redis"]}
image="https://images.unsplash.com/photo-1556742502-ec7c0e9f34b1?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60"
/>
<ProjectCard
title="大数据分析平台"
description="基于Hadoop、Spark的大数据处理平台实现了实时数据分析和可视化。"
tags={["Spark", "Hadoop", "Kafka", "ElasticSearch"]}
image="https://images.unsplash.com/photo-1560807707-8cc77767d783?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60"
/>
</SimpleGrid>
<Box textAlign="center" mt={10}>
<Button
colorScheme={"brand"}
bg={"brand.500"}
rounded={"md"}
px={6}
_hover={{
bg: "brand.600",
}}
onClick={() => window.location.href = '/projects'}
>
</Button>
</Box>
</Container>
</Box>
{/* Call to Action */}
<Box bg={useColorModeValue("brand.500", "brand.600")} py={20}>
<Container maxW={"3xl"}>
<Stack
as={Box}
textAlign={"center"}
gap={{ base: 8, md: 14 }}
>
<Heading
fontWeight={700}
fontSize={{ base: "2xl", sm: "4xl", md: "5xl" }}
lineHeight={"110%"}
color={"white"}
>
</Heading>
<Text color={"white"} fontSize={{ base: "lg", md: "xl" }}>
</Text>
<Stack
direction={"column"}
gap={3}
align={"center"}
alignSelf={"center"}
position={"relative"}
>
<Button
colorScheme={"whiteAlpha"}
bg={"whiteAlpha.800"}
rounded={"full"}
px={6}
_hover={{
bg: "whiteAlpha.900",
}}
color={"brand.500"}
fontWeight="bold"
onClick={() => window.location.href = '/contact'}
>
</Button>
</Stack>
</Stack>
</Container>
</Box>
</Box>
);
}
// Components
const SkillCard = ({ icon, title, description }: { icon: IconType, title: string, description: string }) => {
return (
<VStack
align={"center"}
bg={useColorModeValue("white", "gray.800")}
p={5}
boxShadow={"md"}
rounded={"lg"}
transition={"transform 0.3s ease"}
_hover={{
transform: "translateY(-5px)",
boxShadow: "lg",
}}
>
<Box
p={2}
rounded={"full"}
bg={useColorModeValue("brand.50", "brand.900")}
color={"brand.500"}
mb={4}
>
<Icon as={icon as any} w={10} h={10} />
</Box>
<Heading size={"md"} mb={2}>
{title}
</Heading>
<Text textAlign={"center"} color={"gray.500"}>
{description}
</Text>
</VStack>
);
};
const ProjectCard = ({ title, description, tags, image }: {
title: string,
description: string,
tags: string[],
image: string
}) => {
return (
<Box
bg={useColorModeValue("white", "gray.800")}
p={0}
rounded={"lg"}
overflow={"hidden"}
boxShadow={"md"}
transition={"transform 0.3s ease"}
_hover={{
transform: "translateY(-5px)",
boxShadow: "lg",
}}
>
<Image
src={image}
alt={title}
objectFit={"cover"}
w={"100%"}
h={"200px"}
/>
<VStack p={5} align={"start"} gap={3}>
<Heading size={"md"}>{title}</Heading>
<Text color={"gray.500"}>{description}</Text>
<HStack gap={2}>
{tags.map((tag) => (
<Badge
key={tag}
colorScheme={"brand"}
variant={"subtle"}
px={2}
py={1}
rounded={"md"}
>
{tag}
</Badge>
))}
</HStack>
</VStack>
</Box>
);
};
const Arrow = createIcon({
displayName: "Arrow",
viewBox: "0 0 72 24",
path: (
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.600904 7.08166C0.764293 6.8879 1.01492 6.79004 1.26654 6.82177C2.83216 7.01918 5.20326 7.24581 7.54543 7.23964C9.92491 7.23338 12.1351 6.98464 13.4704 6.32142C13.84 6.13785 14.2885 6.28805 14.4722 6.65692C14.6559 7.02578 14.5052 7.47362 14.1356 7.6572C12.4625 8.48822 9.94063 8.72541 7.54852 8.7317C5.67514 8.73663 3.79547 8.5985 2.29921 8.44247C2.80955 9.59638 3.50943 10.6396 4.24665 11.7384C4.39435 11.9585 4.54354 12.1809 4.69301 12.4068C5.79543 14.0733 6.88128 15.8995 7.1179 18.2636C7.15893 18.6735 6.85928 19.0393 6.4486 19.0805C6.03792 19.1217 5.67174 18.8227 5.6307 18.4128C5.43271 16.4346 4.52957 14.868 3.4457 13.2296C3.3058 13.0181 3.16221 12.8046 3.01684 12.5885C2.05899 11.1646 1.02372 9.62564 0.457909 7.78069C0.383671 7.53862 0.437515 7.27541 0.600904 7.08166ZM5.52039 10.2248C5.77662 9.90161 6.24663 9.84687 6.57018 10.1025C16.4834 17.9344 29.9158 22.4064 42.0781 21.4773C54.1988 20.5514 65.0339 14.2748 69.9746 0.584299C70.1145 0.196597 70.5427 -0.0046455 70.931 0.134813C71.3193 0.274276 71.5206 0.70162 71.3807 1.08932C66.2105 15.4159 54.8056 22.0014 42.1913 22.965C29.6185 23.9254 15.8207 19.3142 5.64226 11.2727C5.31871 11.0171 5.26415 10.5479 5.52039 10.2248Z"
fill="currentColor"
/>
)
});

View File

@@ -0,0 +1,15 @@
import React, { ReactNode } from 'react';
import { ChakraProvider } from '@chakra-ui/react';
import theme from './theme';
interface ChakraProviderProps {
children: ReactNode;
}
export const CustomChakraProvider: React.FC<ChakraProviderProps> = ({ children }) => {
return (
<ChakraProvider theme={theme}>
{children}
</ChakraProvider>
);
};

20
src/theme/theme.ts Normal file
View File

@@ -0,0 +1,20 @@
import { ThemeConfig, extendTheme } from '@chakra-ui/react';
// 创建一个自定义主题
const config: ThemeConfig = {
initialColorMode: 'light',
useSystemColorMode: false,
};
const theme = extendTheme({
config,
colors: {
brand: {
900: "#1a365d",
800: "#153e75",
700: "#2a69ac",
},
},
});
export default theme;