初始化提交

This commit is contained in:
2025-12-13 21:35:46 +08:00
parent 487457e0a9
commit 4573a21f88
54 changed files with 20690 additions and 0 deletions

23
mengyaprofile-frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,92 @@
# 萌芽个人主页 - 前端
基于 React 的个人主页前端应用。
## 功能特性
- ✨ 现代化的界面设计,渐变色背景和流畅动画
- 📱 完美适配移动端和桌面端
- 🎨 精美的卡片式布局
- 🚀 快速响应的用户交互
- 🔗 项目自动获取网站 favicon
- 📋 一键复制联系方式
## 三大模块
### 1. 个人信息模块
展示个人基本信息,包括:
- 昵称
- 头像(带动画效果)
- 个人介绍
- 技术定位
- 个人座右铭
### 2. 精选项目模块
以卡片形式展示项目:
- 项目标题
- 项目简介
- 项目链接
- 项目标签
- 自动获取网站图标
### 3. 联系方式模块
展示联系方式,支持:
- QQ
- 邮箱
- GitHub
- 一键复制
- 直接跳转
## 快速开始
### 安装依赖
```bash
npm install
```
### 启动开发服务器
```bash
npm start
```
应用将在 `http://localhost:3000` 上运行。
### 构建生产版本
```bash
npm run build
```
构建后的文件将生成在 `build` 目录中。
## 配置说明
前端会从后端 API 获取数据,请确保后端服务已启动:
- 后端地址:`http://localhost:5000`
- 如需修改,请编辑 `src/App.js` 中的 API 地址
## 技术栈
- React 19
- CSS3 动画
- Fetch API
- 响应式设计
## 浏览器支持
- Chrome (推荐)
- Firefox
- Safari
- Edge
- 移动端浏览器
## 自定义
如需自定义样式,可以编辑以下文件:
- `src/App.css` - 全局样式
- `src/components/ProfileSection.css` - 个人信息样式
- `src/components/ProjectsSection.css` - 项目展示样式
- `src/components/ContactsSection.css` - 联系方式样式

17568
mengyaprofile-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "mengyaprofile-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#667eea" />
<meta
name="description"
content="萌芽主页 - Full-Stack / Backend / DevOps"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>萌芽主页</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,158 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 隐藏滚动条但保留滚动功能 */
::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
* {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 和 Edge */
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-y: scroll;
overflow-x: hidden;
}
.App {
min-height: 100vh;
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3b6 100%);
position: relative;
overflow-x: hidden;
}
/* 背景图片高斯模糊遮罩层 */
.background-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
backdrop-filter: blur(3px);
-webkit-backdrop-filter: blur(3px);
z-index: 0;
pointer-events: none;
}
/* 背景动画效果 */
.App::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 50%, rgba(255, 255, 255, 0.2) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(255, 255, 255, 0.2) 0%, transparent 50%);
pointer-events: none;
animation: bgFloat 20s ease-in-out infinite;
}
@keyframes bgFloat {
0%, 100% { opacity: 1; transform: translateY(0); }
50% { opacity: 0.8; transform: translateY(-20px); }
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 30px 20px;
position: relative;
z-index: 1;
}
/* 加载状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
color: white;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-container p {
margin-top: 20px;
font-size: 18px;
}
/* 错误状态 */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
color: white;
text-align: center;
padding: 20px;
}
.error-container h2 {
font-size: 32px;
margin-bottom: 16px;
}
.error-container p {
font-size: 18px;
margin-bottom: 12px;
}
.error-hint {
background: rgba(255, 255, 255, 0.1);
padding: 12px 24px;
border-radius: 8px;
margin-top: 20px;
}
/* 页脚 */
.footer {
text-align: center;
padding: 20px 20px;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
position: relative;
z-index: 1;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 20px 15px;
}
.error-container h2 {
font-size: 24px;
}
.error-container p {
font-size: 16px;
}
}

View File

@@ -0,0 +1,149 @@
import React, { useState, useEffect } from 'react';
import './App.css';
import ProfileSection from './components/ProfileSection';
import TechStackSection from './components/TechStackSection';
import ProjectsSection from './components/ProjectsSection';
import ContactsSection from './components/ContactsSection';
import ClickParticle from './components/ClickParticle';
function App() {
const [data, setData] = useState({
profile: null,
techstack: null,
projects: null,
contacts: null
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [backgroundImage, setBackgroundImage] = useState(null);
const [isAdminMode, setIsAdminMode] = useState(false);
useEffect(() => {
// 检查是否为 admin 模式
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
const pathname = window.location.pathname;
if (pathname.includes('/admin') && token === 'shumengya520') {
setIsAdminMode(true);
}
// 从后端API获取所有数据
// 开发环境使用完整URL,生产环境使用相对路径
const apiBaseUrl = process.env.REACT_APP_API_URL ||
(process.env.NODE_ENV === 'development' ? 'http://localhost:5000/api' : '/api');
fetch(`${apiBaseUrl}/all`)
.then(response => {
if (!response.ok) {
throw new Error('网络响应失败');
}
return response.json();
})
.then(data => {
setData(data);
// 设置 favicon
if (data.profile?.favicon) {
let faviconTag = document.querySelector('link[rel="icon"]');
if (!faviconTag) {
faviconTag = document.createElement('link');
faviconTag.rel = 'icon';
document.head.appendChild(faviconTag);
}
faviconTag.href = data.profile.favicon;
}
// 如果启用了本地背景,则获取随机背景图
if (data.profile?.showlocalbackground) {
fetch(`${apiBaseUrl}/random-background`)
.then(res => res.json())
.then(bgData => {
if (bgData.image) {
setBackgroundImage(bgData.image);
}
})
.catch(err => console.error('获取背景图片失败:', err));
}
setLoading(false);
})
.catch(error => {
console.error('获取数据失败:', error);
setError(error.message);
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="loading-container">
<div className="loading-spinner"></div>
<p>加载中...</p>
</div>
);
}
if (error) {
return (
<div className="error-container">
<h2>加载失败</h2>
<p>{error}</p>
<p className="error-hint">请确保后端服务已启动运行 python app.py</p>
</div>
);
}
return (
<div className="App" style={backgroundImage ? {
backgroundImage: `url(${backgroundImage})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundAttachment: 'fixed'
} : {}}>
{/* 鼠标点击粒子效果 */}
<ClickParticle />
{backgroundImage && <div className="background-overlay"></div>}
{/* 管理员模式标识 */}
{isAdminMode && (
<div style={{
position: 'fixed',
top: '10px',
right: '10px',
background: 'rgba(82, 183, 136, 0.9)',
color: 'white',
padding: '8px 16px',
borderRadius: '20px',
fontSize: '14px',
fontWeight: 'bold',
zIndex: 9999,
boxShadow: '0 2px 8px rgba(0,0,0,0.2)'
}}>
🔐 管理员模式
</div>
)}
<div className="container">
{/* 个人信息模块 */}
{data.profile && <ProfileSection profile={data.profile} />}
{/* 技术栈模块 */}
{data.techstack && <TechStackSection techstack={data.techstack} />}
{/* 精选项目模块 */}
{data.projects && <ProjectsSection projects={data.projects.projects} />}
{/* 联系方式模块 */}
{data.contacts && <ContactsSection contacts={data.contacts.contacts} />}
</div>
{/* 页脚 */}
<footer className="footer">
<p><strong>{data.profile?.footer || '© 2025 萌芽个人主页. All rights reserved.'}</strong></p>
</footer>
</div>
);
}
export default App;

View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,66 @@
.click-particle {
position: fixed;
pointer-events: none;
border-radius: 50%;
z-index: 9999;
transform: translate(-50%, -50%);
animation-name: particle-radiate;
animation-timing-function: ease-out;
animation-fill-mode: forwards;
will-change: transform, opacity;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.9);
mix-blend-mode: screen;
}
@keyframes particle-radiate {
0% {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
25% {
transform: translate(
calc(-50% + var(--target-x) * 0.3),
calc(-50% + var(--target-y) * 0.3)
) scale(1.2);
opacity: 0.9;
}
100% {
transform: translate(
calc(-50% + var(--target-x)),
calc(-50% + var(--target-y))
) scale(0);
opacity: 0;
}
}
/* 中心爆炸效果 */
.click-burst {
position: fixed;
pointer-events: none;
width: 20px;
height: 20px;
border-radius: 50%;
z-index: 9998;
transform: translate(-50%, -50%);
background: radial-gradient(circle,
rgba(255, 255, 255, 0.8) 0%,
rgba(255, 255, 255, 0.4) 30%,
rgba(255, 255, 255, 0) 70%
);
animation: burst-effect 0.6s ease-out forwards;
}
@keyframes burst-effect {
0% {
transform: translate(-50%, -50%) scale(0);
opacity: 1;
}
50% {
transform: translate(-50%, -50%) scale(2);
opacity: 0.6;
}
100% {
transform: translate(-50%, -50%) scale(3);
opacity: 0;
}
}

View File

@@ -0,0 +1,74 @@
import React, { useEffect } from 'react';
import './ClickParticle.css';
const ClickParticle = () => {
useEffect(() => {
const handleClick = (e) => {
const colors = [
'#ff6b9d', '#c44569', '#f8b500', '#ffd93d',
'#a8e6cf', '#95d5b2', '#74c69d', '#52b788',
'#6bcf7f', '#4dd599', '#38ada9', '#3fc1c9',
'#5f27cd', '#341f97', '#ee5a6f', '#fc5c65',
'#fed330', '#f7b731', '#fa8231', '#fd79a8'
];
// 更密集的均匀辐射一次点击360°均匀分布
const particleCount = 16 + Math.floor(Math.random() * 8); // 16-24
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'click-particle';
// 均匀角度,无随机抖动,形成整齐的环形辐射
const angle = (Math.PI * 2 * i) / particleCount;
// 半径随机,营造层次
const distance = 90 + Math.random() * 80; // 90-170px
const targetX = Math.cos(angle) * distance;
const targetY = Math.sin(angle) * distance;
// 初始位置(固定坐标系更准确贴合视窗)
particle.style.left = e.clientX + 'px';
particle.style.top = e.clientY + 'px';
// 配色与尺寸
particle.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
const size = 4 + Math.random() * 6; // 4-10px
particle.style.width = size + 'px';
particle.style.height = size + 'px';
// 传递目标位移参数
particle.style.setProperty('--target-x', targetX + 'px');
particle.style.setProperty('--target-y', targetY + 'px');
// 动画时长(更自然的缓出)
const durationMs = 600 + Math.round(Math.random() * 400); // 600-1000ms
particle.style.animationDuration = durationMs / 1000 + 's';
document.body.appendChild(particle);
// 动画完成后清理
setTimeout(() => {
particle.remove();
}, durationMs + 50);
}
// 中心爆炸涟漪
const centerBurst = document.createElement('div');
centerBurst.className = 'click-burst';
centerBurst.style.left = e.clientX + 'px';
centerBurst.style.top = e.clientY + 'px';
document.body.appendChild(centerBurst);
setTimeout(() => centerBurst.remove(), 600);
};
document.addEventListener('click', handleClick, { passive: true });
return () => {
document.removeEventListener('click', handleClick);
};
}, []);
return null;
};
export default ClickParticle;

View File

@@ -0,0 +1,226 @@
.contacts-section {
margin-bottom: 40px;
animation: fadeInUp 0.8s ease-out 0.4s both;
padding: 24px;
border: 2px solid rgba(82, 183, 136, 0.3);
border-radius: 20px;
background: rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 20px rgba(82, 183, 136, 0.1);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section-title {
font-size: 32px;
font-weight: 700;
color: white;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
}
.title-icon {
font-size: 32px;
}
.contacts-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.contact-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
position: relative;
}
.contact-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
}
.contact-icon {
font-size: 28px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #52b788, #95d5b2);
border-radius: 10px;
flex-shrink: 0;
box-shadow: 0 4px 12px rgba(82, 183, 136, 0.3);
position: relative;
overflow: hidden;
}
/* 图片图标样式 */
.contact-icon-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 10px;
}
/* Emoji 图标样式 */
.contact-icon-emoji {
font-size: 28px;
line-height: 1;
}
.contact-info {
flex-grow: 1;
min-width: 0;
}
.contact-label {
font-size: 15px;
font-weight: 600;
color: #2d3748;
margin-bottom: 4px;
}
.contact-value {
font-size: 13px;
color: #4a5568;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contact-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.contact-button {
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: rgba(82, 183, 136, 0.1);
color: #52b788;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
}
.contact-button:hover {
background: linear-gradient(135deg, #52b788, #95d5b2);
color: white;
transform: scale(1.1);
}
.contact-button:active {
transform: scale(0.95);
}
.copy-toast {
position: absolute;
top: -36px;
right: 20px;
background: #48bb78;
color: white;
padding: 6px 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(72, 187, 120, 0.4);
animation: toastSlideIn 0.3s ease-out;
}
@keyframes toastSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.contacts-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.section-title {
font-size: 24px;
margin-bottom: 20px;
}
.contact-card {
padding: 14px;
border-radius: 12px;
gap: 12px;
}
.contact-icon {
width: 40px;
height: 40px;
font-size: 22px;
border-radius: 8px;
}
.contact-icon-img {
border-radius: 8px;
}
.contact-icon-emoji {
font-size: 22px;
}
.contact-label {
font-size: 14px;
}
.contact-value {
font-size: 12px;
}
.contact-actions {
gap: 6px;
}
.contact-button {
width: 28px;
height: 28px;
font-size: 14px;
}
.copy-toast {
right: 50%;
transform: translateX(50%);
}
}
@media (min-width: 769px) and (max-width: 1024px) {
.contacts-grid {
grid-template-columns: repeat(3, 1fr);
}
}

View File

@@ -0,0 +1,103 @@
import React, { useState } from 'react';
import './ContactsSection.css';
function ContactsSection({ contacts }) {
const [copiedType, setCopiedType] = useState(null);
const handleCopy = (value, type) => {
navigator.clipboard.writeText(value).then(() => {
setCopiedType(type);
setTimeout(() => setCopiedType(null), 2000);
});
};
const getContactIcon = (type) => {
const icons = {
qq: '💬',
email: '📧',
github: '🐙',
wechat: '💚',
twitter: '🐦',
linkedin: '💼'
};
return icons[type] || '📱';
};
// 判断 icon 是否为 URL
const isImageUrl = (icon) => {
return icon && (icon.startsWith('http://') || icon.startsWith('https://'));
};
// 渲染图标(支持 URL 和 Emoji)
const renderIcon = (contact) => {
const icon = contact.icon || getContactIcon(contact.type);
if (isImageUrl(icon)) {
return (
<img
src={icon}
alt={contact.label}
className="contact-icon-img"
onError={(e) => {
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'block';
}}
/>
);
}
return <span className="contact-icon-emoji">{icon}</span>;
};
return (
<section className="contacts-section">
<h2 className="section-title">
<span className="title-icon">📮</span>
联系方式
</h2>
<div className="contacts-grid">
{contacts.map((contact, index) => (
<div key={index} className="contact-card">
<div className="contact-icon">
{renderIcon(contact)}
</div>
<div className="contact-info">
<h3 className="contact-label">{contact.label}</h3>
<p className="contact-value">{contact.value}</p>
</div>
<div className="contact-actions">
{contact.link && (
<a
href={contact.link}
target="_blank"
rel="noopener noreferrer"
className="contact-button contact-visit"
title="访问"
>
🔗
</a>
)}
<button
onClick={() => handleCopy(contact.value, contact.type)}
className="contact-button contact-copy"
title="复制"
>
{copiedType === contact.type ? '✓' : '📋'}
</button>
</div>
{copiedType === contact.type && (
<div className="copy-toast">已复制!</div>
)}
</div>
))}
</div>
</section>
);
}
export default ContactsSection;

View File

@@ -0,0 +1,155 @@
.profile-section {
margin-bottom: 40px;
}
.profile-card {
background: transparent;
border-radius: 24px;
padding: 40px 30px;
text-align: center;
box-shadow: none;
animation: fadeInUp 0.8s ease-out;
position: relative;
overflow: hidden;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.profile-avatar-container {
position: relative;
display: inline-block;
margin-bottom: 20px;
}
.profile-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 5px solid white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 1;
animation: avatarPulse 3s ease-in-out infinite;
}
@keyframes avatarPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.avatar-ring {
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
border-radius: 50%;
background: linear-gradient(135deg, #52b788, #95d5b2);
opacity: 0.3;
animation: ringPulse 3s ease-in-out infinite;
}
@keyframes ringPulse {
0%, 100% { transform: scale(1); opacity: 0.3; }
50% { transform: scale(1.1); opacity: 0.5; }
}
.profile-nickname {
font-size: 36px;
font-weight: 700;
color: #2d3748;
margin-bottom: 12px;
background: linear-gradient(135deg, #52b788, #95d5b2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.profile-position {
margin-bottom: 24px;
}
.position-badge {
display: inline-block;
background: linear-gradient(135deg, #52b788, #95d5b2);
color: white;
padding: 8px 24px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.5px;
box-shadow: 0 4px 15px rgba(82, 183, 136, 0.4);
}
.profile-introduction {
font-size: 16px;
line-height: 1.6;
color: #2d3748;
margin-bottom: 0;
max-width: 600px;
margin-left: auto;
margin-right: auto;
text-shadow: 0 1px 3px rgba(255, 255, 255, 0.8);
font-weight: 500;
}
.profile-motto {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 16px 24px;
background: linear-gradient(135deg, rgba(82, 183, 136, 0.1), rgba(149, 213, 178, 0.1));
border-radius: 12px;
margin-top: 24px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.motto-icon {
font-size: 24px;
}
.motto-text {
font-size: 16px;
color: #2d6a4f;
font-weight: 500;
font-style: italic;
}
/* 响应式设计 */
@media (max-width: 768px) {
.profile-card {
padding: 30px 20px;
}
.profile-avatar {
width: 100px;
height: 100px;
}
.profile-nickname {
font-size: 28px;
}
.profile-introduction {
font-size: 15px;
}
.profile-motto {
flex-direction: column;
gap: 8px;
}
}

View File

@@ -0,0 +1,25 @@
import React from 'react';
import './ProfileSection.css';
function ProfileSection({ profile }) {
return (
<section className="profile-section">
<div className="profile-card">
<div className="profile-avatar-container">
<img
src={profile.avatar}
alt={profile.nickname}
className="profile-avatar"
/>
<div className="avatar-ring"></div>
</div>
<h1 className="profile-nickname">{profile.nickname}</h1>
<p className="profile-introduction">{profile.introduction}</p>
</div>
</section>
);
}
export default ProfileSection;

View File

@@ -0,0 +1,238 @@
.projects-section {
margin-bottom: 40px;
animation: fadeInUp 0.8s ease-out 0.2s both;
padding: 24px;
border: 2px solid rgba(82, 183, 136, 0.3);
border-radius: 20px;
background: rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 20px rgba(82, 183, 136, 0.1);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section-title {
font-size: 32px;
font-weight: 700;
color: white;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
}
.title-icon {
font-size: 36px;
display: inline-block;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.projects-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
}
.project-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
}
.develop-badge {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
cursor: help;
color: #52b788;
opacity: 0.85;
transition: opacity 0.2s ease;
}
.develop-badge:hover {
opacity: 1;
}
.develop-badge svg {
display: block;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
.project-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #52b788, #95d5b2);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.project-card:hover::before {
transform: scaleX(1);
}
.project-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.project-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.project-icon {
width: 40px;
height: 40px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
.project-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.project-title {
font-size: 18px;
font-weight: 700;
color: #2d3748;
margin: 0;
flex: 1;
}
.project-description {
font-size: 14px;
line-height: 1.5;
color: #4a5568;
margin-bottom: 12px;
flex-grow: 1;
}
.project-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
.project-tag {
display: inline-block;
padding: 3px 10px;
background: linear-gradient(135deg, rgba(82, 183, 136, 0.15), rgba(149, 213, 178, 0.15));
color: #2d6a4f;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
}
.project-link-indicator {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 12px;
border-top: 1px solid #e2e8f0;
color: #52b788;
font-weight: 600;
font-size: 13px;
}
.arrow {
transition: transform 0.3s ease;
font-size: 18px;
}
.project-card:hover .arrow {
transform: translateX(4px);
}
/* 响应式设计 */
@media (max-width: 768px) {
.projects-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.section-title {
font-size: 28px;
}
.project-card {
padding: 16px;
}
.develop-badge {
top: 8px;
right: 8px;
}
.develop-badge svg {
width: 16px;
height: 16px;
}
.project-header {
gap: 10px;
margin-bottom: 10px;
}
.project-icon {
width: 36px;
height: 36px;
}
.project-title {
font-size: 16px;
}
.project-description {
font-size: 13px;
}
.project-tag {
font-size: 11px;
padding: 2px 8px;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
.projects-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1025px) and (max-width: 1440px) {
.projects-grid {
grid-template-columns: repeat(4, 1fr);
}
}

View File

@@ -0,0 +1,102 @@
import React, { useState, useEffect } from 'react';
import './ProjectsSection.css';
function ProjectsSection({ projects }) {
const [hoveredId, setHoveredId] = useState(null);
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
// 检查 URL 参数
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
const pathname = window.location.pathname;
// 检查是否为 /admin 路径且 token 正确
if (pathname.includes('/admin') && token === 'shumengya520') {
setIsAdmin(true);
}
}, []);
const getFavicon = (url) => {
try {
const domain = new URL(url).origin;
return `${domain}/favicon.ico`;
} catch {
return 'https://api.iconify.design/mdi:web.svg';
}
};
// 过滤项目
// 1. 如果 show 为 false,则不显示
// 2. 如果 admin 为 true 且不是管理员模式,则不显示
const filteredProjects = projects.filter(project => {
// 首先检查 show 字段,如果为 false 则直接不显示
if (project.show === false) {
return false;
}
// 然后检查 admin 权限
if (project.admin === true && !isAdmin) {
return false; // 隐藏需要 admin 权限的项目
}
return true; // 显示其他所有项目
});
return (
<section className="projects-section">
<h2 className="section-title">
<span className="title-icon">🎯</span>
精选项目
</h2>
<div className="projects-grid">
{filteredProjects.map(project => (
<a
key={project.id}
href={project.link}
target="_blank"
rel="noopener noreferrer"
className={`project-card ${hoveredId === project.id ? 'hovered' : ''}`}
onMouseEnter={() => setHoveredId(project.id)}
onMouseLeave={() => setHoveredId(null)}
>
{project.develop === true && (
<div className="develop-badge" title="独立开发">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.7803 3.03039L20.4697 9.71982C21.0508 10.3009 21.0508 11.2451 20.4697 11.8262L11.8262 20.4697C11.2451 21.0508 10.3009 21.0508 9.71982 20.4697L3.03039 13.7803C2.44928 13.1992 2.44928 12.255 3.03039 11.6739L11.6739 3.03039C12.255 2.44928 13.1992 2.44928 13.7803 3.03039Z" fill="currentColor"/>
<path d="M5 20L9 16L13 20L9 24L5 20Z" fill="currentColor"/>
</svg>
</div>
)}
<div className="project-header">
<div className="project-icon">
<img
src={project.icon || getFavicon(project.link)}
alt={project.title}
onError={(e) => {
e.target.src = 'https://api.iconify.design/mdi:web.svg';
}}
/>
</div>
<h3 className="project-title">{project.title}</h3>
</div>
<p className="project-description">{project.description}</p>
{project.tags && project.tags.length > 0 && (
<div className="project-tags">
{project.tags.map((tag, index) => (
<span key={index} className="project-tag">{tag}</span>
))}
</div>
)}
</a>
))}
</div>
</section>
);
}
export default ProjectsSection;

View File

@@ -0,0 +1,120 @@
.techstack-section {
margin-bottom: 40px;
animation: fadeInUp 0.8s ease-out 0.15s both;
padding: 24px;
border: 2px solid rgba(82, 183, 136, 0.3);
border-radius: 20px;
background: rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 20px rgba(82, 183, 136, 0.1);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section-title {
font-size: 32px;
font-weight: 700;
color: white;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
}
.title-icon {
font-size: 36px;
display: inline-block;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.techstack-container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.tech-items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
align-items: center;
justify-items: center;
}
.tech-item {
transition: all 0.3s ease;
cursor: pointer;
width: 100%;
display: flex;
justify-content: center;
padding: 12px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.25);
}
.tech-item a {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
text-decoration: none;
}
.tech-item img {
height: 32px;
max-width: 100%;
display: block;
transition: transform 0.3s ease, filter 0.3s ease;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
object-fit: contain;
}
.tech-item:hover {
transform: translateY(-3px);
background: rgba(255, 255, 255, 0.25);
box-shadow: 0 4px 15px rgba(82, 183, 136, 0.2);
}
.tech-item:hover img {
filter: brightness(1.1) drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15));
}
/* 响应式设计 */
@media (max-width: 768px) {
.section-title {
font-size: 28px;
}
.techstack-container {
padding: 20px;
}
.tech-items {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 10px;
}
.tech-item img {
height: 28px;
}
}

View File

@@ -0,0 +1,47 @@
import React from 'react';
import './TechStackSection.css';
function TechStackSection({ techstack }) {
if (!techstack || !techstack.items) return null;
return (
<section className="techstack-section">
<h2 className="section-title">
<span className="title-icon">🛠</span>
{techstack.title}
</h2>
<div className="techstack-container">
<div className="tech-items">
{techstack.items.map((item, idx) => (
<div key={idx} className="tech-item">
{item.link ? (
<a
href={item.link}
target="_blank"
rel="noopener noreferrer"
title={item.name}
>
<img
src={item.icon}
alt={item.name}
loading="lazy"
/>
</a>
) : (
<img
src={item.icon}
alt={item.name}
title={item.name}
loading="lazy"
/>
)}
</div>
))}
</div>
</div>
</section>
);
}
export default TechStackSection;

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';