init
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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
87
api/README.md
Normal 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
1152
api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
api/package.json
Normal file
20
api/package.json
Normal 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
97
api/server.js
Normal 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
1200
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -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"
|
||||
},
|
||||
|
||||
33
src/App.tsx
33
src/App.tsx
@@ -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
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;
|
||||
@@ -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>
|
||||
<CustomChakraProvider>
|
||||
<App />
|
||||
</CustomChakraProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
||||
354
src/pages/Home.tsx
Normal file
354
src/pages/Home.tsx
Normal 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"
|
||||
/>
|
||||
)
|
||||
});
|
||||
15
src/theme/ChakraContext.tsx
Normal file
15
src/theme/ChakraContext.tsx
Normal 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
20
src/theme/theme.ts
Normal 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;
|
||||
Reference in New Issue
Block a user