初始化提交

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

41
.dockerignore Normal file
View File

@@ -0,0 +1,41 @@
# Node 相关
mengyaprofile-frontend/node_modules
mengyaprofile-frontend/build
mengyaprofile-frontend/.env.local
mengyaprofile-frontend/.env.development.local
mengyaprofile-frontend/.env.test.local
mengyaprofile-frontend/.env.production.local
mengyaprofile-frontend/npm-debug.log*
mengyaprofile-frontend/yarn-debug.log*
mengyaprofile-frontend/yarn-error.log*
# Python 相关
mengyaprofile-backend/__pycache__
mengyaprofile-backend/*.pyc
mengyaprofile-backend/*.pyo
mengyaprofile-backend/*.pyd
mengyaprofile-backend/.Python
mengyaprofile-backend/env
mengyaprofile-backend/venv
mengyaprofile-backend/.env
# Git 相关
.git
.gitignore
.gitattributes
# IDE 相关
.vscode
.idea
*.swp
*.swo
*~
# OS 相关
.DS_Store
Thumbs.db
# 其他
*.bat
*.md
README.md

42
Dockerfile Normal file
View File

@@ -0,0 +1,42 @@
# 多阶段构建 Dockerfile
# 阶段1: 构建前端
FROM node:18-alpine AS frontend-builder
WORKDIR /app/frontend
# 复制前端文件
COPY mengyaprofile-frontend/package*.json ./
RUN npm install
COPY mengyaprofile-frontend/ ./
RUN npm run build
# 阶段2: 构建后端并整合
FROM python:3.11-slim
WORKDIR /app
# 安装 Python 依赖
COPY mengyaprofile-backend/requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# 复制后端代码
COPY mengyaprofile-backend/ ./backend/
# 从前端构建阶段复制构建产物
COPY --from=frontend-builder /app/frontend/build ./frontend/build
# 创建数据目录(用于持久化)
RUN mkdir -p /app/data
# 暴露端口
EXPOSE 5000
# 设置环境变量
ENV PYTHONUNBUFFERED=1
ENV DATA_DIR=/app/data
ENV RUN_MODE=production
# 启动命令
WORKDIR /app/backend
CMD ["python", "app.py"]

270
README.md Normal file
View File

@@ -0,0 +1,270 @@
# 萌芽个人主页
一个现代化的个人主页网站,采用前后端分离架构,支持 Docker 一键部署。
## 🚀 快速部署 (Docker)
### 一键启动
**Windows:**
```bash
docker-deploy.bat
```
**Linux/Mac:**
```bash
chmod +x docker-deploy.sh
./docker-deploy.sh
```
**或使用 Docker Compose:**
```bash
docker-compose up -d --build
```
**访问应用:**
- 主页: http://localhost:5000
- 管理员: http://localhost:5000/admin?token=shumengya520
📖 详细文档: [Docker 部署指南](DOCKER_README.md) | [快速开始](QUICKSTART.md)
---
## 项目简介
这是一个功能完整、设计精美的个人主页系统,展示个人信息、精选项目和联系方式。
### 特性亮点
- 🎨 **现代化设计**: 渐变色背景、流畅动画、精美卡片布局
- 📱 **响应式布局**: 完美适配桌面端和移动端
- 🔄 **前后端分离**: React + Python Flask 架构
- 🐳 **Docker 支持**: 一键部署,开箱即用
- 💾 **数据持久化**: 配置文件外部存储
-**快速灵活**: 通过 JSON 配置文件轻松管理内容
- 🎯 **三大模块**: 个人信息、精选项目、联系方式
- 🔐 **权限控制**: 管理员模式隐藏私密项目
## 项目结构
```
萌芽个人主页/
├── mengyaprofile-backend/ # 后端 Python Flask 项目
│ ├── app.py # Flask 应用主文件
│ ├── requirements.txt # Python 依赖
│ └── data/ # 数据配置文件
│ ├── profile.json # 个人信息
│ ├── projects.json # 项目列表
│ └── contacts.json # 联系方式
└── mengyaprofile-frontend/ # 前端 React 项目
├── public/ # 静态资源
├── src/ # 源代码
│ ├── App.js # 主应用
│ ├── App.css # 全局样式
│ └── components/ # 组件目录
│ ├── ProfileSection.js # 个人信息组件
│ ├── ProjectsSection.js # 项目展示组件
│ └── ContactsSection.js # 联系方式组件
└── package.json # 项目配置
```
## 快速开始
### 前置要求
- Python 3.7+
- Node.js 14+
- npm 或 yarn
### 1. 启动后端服务
```bash
# 进入后端目录
cd mengyaprofile-backend
# 安装依赖
pip install -r requirements.txt
# 运行服务
python app.py
```
后端服务将运行在 `http://localhost:5000`
### 2. 启动前端应用
```bash
# 进入前端目录
cd mengyaprofile-frontend
# 安装依赖
npm install
# 启动开发服务器
npm start
```
前端应用将运行在 `http://localhost:3000`
### 3. 访问应用
在浏览器中打开 `http://localhost:3000` 即可查看你的个人主页!
## 功能模块
### 1⃣ 个人信息模块
展示个人基本信息:
- ✨ 昵称
- 🖼️ 头像(支持动画效果)
- 📝 个人介绍
- 💼 技术定位Full-Stack / Backend / DevOps
- 💡 个人座右铭
**配置文件**: `mengyaprofile-backend/data/profile.json`
### 2⃣ 精选项目模块
以卡片形式展示项目:
- 📦 项目标题
- 📄 项目简介
- 🔗 项目链接
- 🏷️ 项目标签
- 🎯 自动获取网站图标favicon
**配置文件**: `mengyaprofile-backend/data/projects.json`
### 3⃣ 联系方式模块
展示多种联系方式:
- 💬 QQ
- 📧 邮箱
- 🐙 GitHub
- 📋 一键复制功能
- 🔗 直接跳转链接
**配置文件**: `mengyaprofile-backend/data/contacts.json`
## 自定义配置
### 修改个人信息
编辑 `mengyaprofile-backend/data/profile.json`:
```json
{
"nickname": "你的昵称",
"avatar": "头像URL",
"introduction": "个人介绍",
"position": "Full-Stack / Backend / DevOps",
"motto": "你的座右铭"
}
```
### 添加项目
编辑 `mengyaprofile-backend/data/projects.json`:
```json
{
"projects": [
{
"id": 1,
"title": "项目名称",
"description": "项目简介",
"link": "https://your-project.com",
"icon": "",
"tags": ["标签1", "标签2"]
}
]
}
```
### 更新联系方式
编辑 `mengyaprofile-backend/data/contacts.json`:
```json
{
"contacts": [
{
"type": "qq",
"label": "QQ",
"value": "你的QQ号",
"link": "tencent://message/?uin=你的QQ号",
"icon": "💬"
}
]
}
```
## 技术栈
### 前端
- React 19
- CSS3动画、渐变、响应式布局
- Fetch API
### 后端
- Python 3
- Flask 3.0
- Flask-CORS跨域支持
## API 接口
后端提供以下 RESTful API
- `GET /api/profile` - 获取个人信息
- `GET /api/projects` - 获取项目列表
- `GET /api/contacts` - 获取联系方式
- `GET /api/all` - 获取所有数据
## 部署建议
### 后端部署
推荐使用以下方式部署后端:
- Heroku
- AWS EC2
- 阿里云 ECS
- 使用 Gunicorn 作为生产服务器
### 前端部署
推荐使用以下方式部署前端:
- Vercel
- Netlify
- GitHub Pages
- Nginx 静态服务器
**注意**: 部署前需要修改前端 `src/App.js` 中的 API 地址为生产环境地址。
## 浏览器支持
- ✅ Chrome推荐
- ✅ Firefox
- ✅ Safari
- ✅ Edge
- ✅ 移动端浏览器
## 开发计划
- [ ] 添加博客模块
- [ ] 添加深色模式
- [ ] 添加多语言支持
- [ ] 添加后台管理系统
- [ ] 添加访问统计
- [ ] 添加留言板功能
## 许可证
MIT License
## 作者
萌芽
---
如有问题或建议,欢迎提 Issue

42
build-frontend.bat Normal file
View File

@@ -0,0 +1,42 @@
@echo off
setlocal
title Mengya Profile - Build Frontend
echo [INFO] Building frontend assets...
cd /d "%~dp0mengyaprofile-frontend"
where npm >nul 2>nul
if errorlevel 1 (
echo [ERROR] npm not found. Install Node.js and npm.
exit /b 1
)
if not exist "package.json" (
echo [ERROR] package.json not found in %CD%
exit /b 1
)
if not exist "node_modules" goto install_deps
goto do_build
:install_deps
echo [INFO] Installing dependencies...
npm install --prefix "%~dp0mengyaprofile-frontend"
if errorlevel 1 goto deps_fail
:do_build
echo [INFO] Run build: npm run build
npm run build --prefix "%~dp0mengyaprofile-frontend"
if errorlevel 1 goto build_fail
echo [INFO] Build done at %CD%\build
goto end
:deps_fail
echo [ERROR] Dependency install failed.
goto end
:build_fail
echo [ERROR] Build failed.
goto end
:end
endlocal

28
docker-compose.yml Normal file
View File

@@ -0,0 +1,28 @@
version: '3.8'
services:
mengya-profile:
build: .
container_name: mengya-profile
restart: unless-stopped
ports:
- "5000:5000" # 后端 API 端口
volumes:
- /shumengya/docker/storage/mengyaprofile/data:/app/data:rw
- /shumengya/docker/storage/mengyaprofile/background:/app/frontend/build/background:rw
environment:
- PYTHONUNBUFFERED=1
- DATA_DIR=/app/data
- RUN_MODE=production
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/api/all')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- mengya-network
networks:
mengya-network:
driver: bridge

176
docker-deploy.sh Normal file
View File

@@ -0,0 +1,176 @@
#!/bin/bash
# 萌芽个人主页 - Docker 部署脚本 (Linux/Mac)
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 检查 Docker 是否安装
if ! command -v docker &> /dev/null; then
echo -e "${RED}[ERROR]${NC} Docker 未安装"
echo "请先安装 Docker: https://docs.docker.com/get-docker/"
exit 1
fi
# 检查 Docker 是否运行
if ! docker ps &> /dev/null; then
echo -e "${RED}[ERROR]${NC} Docker 服务未运行"
echo "请启动 Docker 后重试"
exit 1
fi
# 菜单函数
show_menu() {
clear
echo "========================================"
echo " 萌芽个人主页 - Docker 部署菜单"
echo "========================================"
echo
echo "[1] 构建并启动容器 (首次部署)"
echo "[2] 启动容器"
echo "[3] 停止容器"
echo "[4] 重启容器"
echo "[5] 查看日志"
echo "[6] 查看容器状态"
echo "[7] 停止并删除容器"
echo "[8] 重新构建镜像"
echo "[0] 退出"
echo
}
# 构建并启动
build_and_start() {
echo
echo -e "${YELLOW}[INFO]${NC} 创建持久化目录..."
mkdir -p /shumengya/docker/storage/mengyaprofile/data
mkdir -p /shumengya/docker/storage/mengyaprofile/background
echo -e "${YELLOW}[INFO]${NC} 复制初始数据..."
if [ ! -f /shumengya/docker/storage/mengyaprofile/data/profile.json ]; then
cp -r mengyaprofile-backend/data/* /shumengya/docker/storage/mengyaprofile/data/
fi
if [ -d mengyaprofile-frontend/public/background ] && [ "$(ls -A mengyaprofile-frontend/public/background)" ]; then
cp -r mengyaprofile-frontend/public/background/* /shumengya/docker/storage/mengyaprofile/background/ 2>/dev/null || true
fi
echo -e "${YELLOW}[INFO]${NC} 正在构建并启动容器..."
if docker-compose up -d --build; then
echo
echo -e "${GREEN}[SUCCESS]${NC} 启动成功!"
echo "访问地址: http://localhost:5000"
echo "管理员模式: http://localhost:5000/admin?token=shumengya520"
else
echo -e "${RED}[ERROR]${NC} 启动失败"
fi
echo
read -p "按回车键继续..."
}
# 启动容器
start() {
echo
echo -e "${YELLOW}[INFO]${NC} 正在启动容器..."
if docker-compose up -d; then
echo -e "${GREEN}[SUCCESS]${NC} 启动成功!"
else
echo -e "${RED}[ERROR]${NC} 启动失败"
fi
read -p "按回车键继续..."
}
# 停止容器
stop() {
echo
echo -e "${YELLOW}[INFO]${NC} 正在停止容器..."
docker-compose stop
echo -e "${GREEN}[SUCCESS]${NC} 已停止"
read -p "按回车键继续..."
}
# 重启容器
restart() {
echo
echo -e "${YELLOW}[INFO]${NC} 正在重启容器..."
docker-compose restart
echo -e "${GREEN}[SUCCESS]${NC} 已重启"
read -p "按回车键继续..."
}
# 查看日志
logs() {
echo
echo -e "${YELLOW}[INFO]${NC} 显示日志 (按 Ctrl+C 退出)..."
echo
docker-compose logs -f
read -p "按回车键继续..."
}
# 查看状态
status() {
echo
echo -e "${YELLOW}[INFO]${NC} 容器状态:"
echo
docker-compose ps
echo
echo -e "${YELLOW}[INFO]${NC} 资源占用:"
docker stats --no-stream mengya-profile
echo
read -p "按回车键继续..."
}
# 删除容器
remove() {
echo
echo -e "${YELLOW}[WARNING]${NC} 将停止并删除容器(数据不会丢失)"
read -p "确认删除? (y/n): " confirm
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
echo
echo -e "${YELLOW}[INFO]${NC} 正在删除容器..."
docker-compose down
echo -e "${GREEN}[SUCCESS]${NC} 已删除"
fi
read -p "按回车键继续..."
}
# 重新构建
rebuild() {
echo
echo -e "${YELLOW}[INFO]${NC} 正在重新构建镜像..."
docker-compose down
if docker-compose build --no-cache && docker-compose up -d; then
echo -e "${GREEN}[SUCCESS]${NC} 重新构建完成!"
else
echo -e "${RED}[ERROR]${NC} 构建失败"
fi
read -p "按回车键继续..."
}
# 主循环
while true; do
show_menu
read -p "请选择操作 (0-8): " choice
case $choice in
1) build_and_start ;;
2) start ;;
3) stop ;;
4) restart ;;
5) logs ;;
6) status ;;
7) remove ;;
8) rebuild ;;
0)
echo
echo "感谢使用萌芽个人主页 Docker 部署工具!"
exit 0
;;
*)
echo -e "${RED}无效选择${NC}"
sleep 1
;;
esac
done

View File

@@ -0,0 +1,14 @@
# 环境配置文件示例
# 复制此文件为 .env 并修改相应的值
# Flask 配置
FLASK_APP=app.py
FLASK_ENV=development
FLASK_DEBUG=True
# 服务器配置
HOST=0.0.0.0
PORT=5000
# CORS 配置(前端地址)
FRONTEND_URL=http://localhost:3000

42
mengyaprofile-backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Flask
instance/
.webassets-cache
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log

View File

@@ -0,0 +1,33 @@
# 萌芽个人主页 - 后端
基于 Flask 的个人主页后端 API 服务。
## 安装依赖
```bash
pip install -r requirements.txt
```
## 运行服务
```bash
python app.py
```
服务将运行在 `http://localhost:5000`
## API 接口
- `GET /api/profile` - 获取个人基本信息
- `GET /api/projects` - 获取精选项目列表
- `GET /api/contacts` - 获取联系方式
- `GET /api/all` - 获取所有数据
## 数据配置
数据存储在 `data` 目录下的 JSON 文件中:
- `profile.json` - 个人信息
- `projects.json` - 项目列表
- `contacts.json` - 联系方式
可根据需要编辑这些文件来更新网站内容。

View File

@@ -0,0 +1,173 @@
from flask import Flask, jsonify, send_from_directory, request
from flask_cors import CORS
import json
import os
import random
# 检测运行模式:通过环境变量控制
RUN_MODE = os.environ.get('RUN_MODE', 'development') # development 或 production
# 根据运行模式配置
if RUN_MODE == 'production':
# 生产环境:使用构建后的前端
FRONTEND_BUILD_PATH = os.path.join(os.path.dirname(__file__), '..', 'frontend', 'build')
app = Flask(__name__, static_folder=FRONTEND_BUILD_PATH, static_url_path='')
BACKGROUND_DIR = os.path.join(FRONTEND_BUILD_PATH, 'background')
else:
# 开发环境:不服务前端,只提供 API
app = Flask(__name__)
BACKGROUND_DIR = os.path.join(os.path.dirname(__file__), '..', 'mengyaprofile-frontend', 'public', 'background')
CORS(app) # 允许跨域请求
# 数据文件路径 - 支持环境变量配置
DATA_DIR = os.environ.get('DATA_DIR', os.path.join(os.path.dirname(__file__), 'data'))
def load_json_file(filename):
"""加载JSON文件"""
try:
with open(os.path.join(DATA_DIR, filename), 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
return None
except Exception as e:
print(f"Error loading {filename}: {e}")
return None
@app.route('/api/profile', methods=['GET'])
def get_profile():
"""获取个人基本信息"""
data = load_json_file('profile.json')
if data:
return jsonify(data)
return jsonify({"error": "Profile没有找到"}), 404
@app.route('/api/projects', methods=['GET'])
def get_projects():
"""获取精选项目列表"""
data = load_json_file('projects.json')
if data:
return jsonify(data)
return jsonify({"error": "Projects没有找到"}), 404
@app.route('/api/contacts', methods=['GET'])
def get_contacts():
"""获取联系方式"""
data = load_json_file('contacts.json')
if data:
return jsonify(data)
return jsonify({"error": "Contacts没有找到"}), 404
@app.route('/api/techstack', methods=['GET'])
def get_techstack():
"""获取技术栈"""
data = load_json_file('techstack.json')
if data:
return jsonify(data)
return jsonify({"error": "Tech stack没有找到"}), 404
@app.route('/api/random-background', methods=['GET'])
def get_random_background():
"""获取随机背景图片"""
try:
# 获取背景图片目录中的所有图片
if os.path.exists(BACKGROUND_DIR):
images = [f for f in os.listdir(BACKGROUND_DIR)
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.gif'))]
if images:
random_image = random.choice(images)
return jsonify({"image": f"/background/{random_image}"})
return jsonify({"image": None})
except Exception as e:
print(f"获取随机背景出错: {e}")
return jsonify({"image": None})
@app.route('/api/all', methods=['GET'])
def get_all():
"""获取所有数据"""
profile = load_json_file('profile.json')
projects = load_json_file('projects.json')
contacts = load_json_file('contacts.json')
techstack = load_json_file('techstack.json')
return jsonify({
"profile": profile,
"techstack": techstack,
"projects": projects,
"contacts": contacts
})
@app.route('/', methods=['GET'])
def index():
"""服务前端页面或API信息"""
if RUN_MODE == 'production' and app.static_folder:
# 生产环境,返回前端页面
return send_from_directory(app.static_folder, 'index.html')
else:
# 开发环境,返回API信息
return jsonify({
"message": "萌芽主页 后端API - 开发模式",
"author": "树萌芽",
"version": "1.0.0",
"mode": "development",
"note": "前端开发服务器运行在 http://localhost:3000",
"endpoints": {
"/api/profile": "获取个人信息",
"/api/techstack": "获取技术栈",
"/api/projects": "获取项目列表",
"/api/contacts": "获取联系方式",
"/api/random-background": "获取随机背景图片",
"/api/all": "获取所有数据"
}
})
@app.route('/admin')
def admin():
"""服务管理员页面(也是前端)"""
if RUN_MODE == 'production' and app.static_folder:
return send_from_directory(app.static_folder, 'index.html')
else:
return jsonify({
"error": "开发模式",
"note": "请访问 http://localhost:3000/admin?token=shumengya520"
}), 404
@app.route('/api')
def api_info():
"""API信息"""
return jsonify({
"message": "萌芽主页 后端API",
"author":"树萌芽",
"version": "1.0.0",
"endpoints": {
"/api/profile": "获取个人信息",
"/api/techstack": "获取技术栈",
"/api/projects": "获取项目列表",
"/api/contacts": "获取联系方式",
"/api/random-background": "获取随机背景图片",
"/api/all": "获取所有数据"
}
})
# 处理前端路由 - 所有非API请求都返回 index.html
@app.errorhandler(404)
def not_found(e):
"""处理404错误"""
# 检查是否为API请求
if request.path.startswith('/api'):
return jsonify({"error": "API endpoint not found"}), 404
# 非API请求
if RUN_MODE == 'production' and app.static_folder:
# 生产环境返回前端页面(支持前端路由)
return send_from_directory(app.static_folder, 'index.html')
# 开发环境
return jsonify({
"error": "页面未找到",
"mode": "development",
"note": "开发环境请访问 http://localhost:3000"
}), 404
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)

View File

@@ -0,0 +1,39 @@
{
"contacts": [
{
"type": "qq",
"label": "QQ",
"value": "3205788256",
"link": "tencent://message/?uin=123456789",
"icon": "https://img.shumengya.top/i/2025/11/02/69076687211f9.webp"
},
{
"type": "email",
"label": "QQ邮箱",
"value": "3205788256@qq.com",
"link": "mailto:3205788256@qq.com",
"icon": "https://img.shumengya.top/i/2025/11/02/690766903514e.webp"
},
{
"type": "email",
"label": "谷歌邮箱",
"value": "shumengya666@gmail.com",
"link": "mailto:shumengya666@gmail.com",
"icon": "https://img.shumengya.top/i/2025/11/03/6908321840ea6.webp"
},
{
"type": "personprofile",
"label": "个人主页",
"value": "shumengya.top",
"link": "https://shumengya.top",
"icon": "https://img.shumengya.top/i/2025/11/03/690836f3c87b2.png"
},
{
"type": "github",
"label": "GitHub",
"value": "github.com/shumengya",
"link": "https://github.com/shumengya",
"icon": "https://img.shumengya.top/i/2025/11/03/69083414ceb01.webp"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"favicon":"https://img.shumengya.top/i/2025/11/03/690836f3c87b2.png",
"nickname": "树萌芽吖",
"avatar": "https://img.shumengya.top/i/2025/11/02/69073c018174e.webp",
"introduction": "热爱编程,享受创造的过程。持续学习新技术,用代码实现想法。",
"footer":"© 2025 萌芽主页-蜀ICP备2025151694号",
"showlocalbackground":true
}

View File

@@ -0,0 +1,170 @@
{
"projects": [
{
"id": 1,
"title": "萌芽盘",
"description": "一个轻量级在线网盘,支持文件上传、下载、分享等功能",
"link": "https://pan.shumengya.top",
"icon": "https://image.shumengya.top/i/2025/11/02/openlist.png",
"tags": ["网盘","OpenList"],
"admin":false,
"show":true,
"develop":false
},
{
"id": 2,
"title": "萌芽主页",
"description": "一个简洁美观的个人主页,展示个人信息和项目",
"link": "https://shumengya.top",
"icon": "https://img.shumengya.top/i/2025/11/03/690836f3c87b2.png",
"tags": ["个人主页"],
"admin":false,
"show":true,
"develop":true
},
{
"id": 3,
"title": "萌芽Git仓库",
"description": "自部署私有化Git仓库",
"link": "https://repo.shumengya.top",
"icon": "https://image.shumengya.top/i/2025/11/02/gitea.png",
"tags": ["Gitea", "GitHub"],
"admin":false,
"show":true,
"develop":false
},
{
"id": 4,
"title": "萌芽快传",
"description": "像取快递一样方便的寄送文件",
"link": "https://send.shumengya.top",
"icon": "https://image.shumengya.top/i/2025/11/02/filecodebox.png",
"tags": ["FileCodeBox", "文件快传"],
"admin":false,
"show":true,
"develop":false
},
{
"id": 5,
"title": "萌芽图床",
"description": "简单易用的图床,将您的图片转化为一段网页链接",
"link": "https://img.shumengya.top",
"icon": "https://image.shumengya.top/i/2025/11/02/mengyaimgbed.png",
"tags": [ "图床"],
"admin":false,
"show":true,
"develop":false
},
{
"id": 6,
"title": "萌芽笔记",
"description": "展示自己学习过程中的一些MarkDown笔记",
"link": "https://note.shumengya.top",
"icon": "https://image.shumengya.top/i/2025/11/02/mengyanote.png",
"tags": [ "笔记","Obsidion"],
"admin":false,
"show":true,
"develop":true
},
{
"id": 7,
"title": "萌芽作品集",
"description": "展示个人制作的一些小创意和小项目",
"link": "https://work.shumengya.top",
"icon": "https://img.shumengya.top/i/2025/11/02/69074f8f5ed5e.png",
"tags": [ "作品集"],
"admin":false,
"show":true,
"develop":true
},
{
"id": 8,
"title": "万象口袋",
"description": "一款跨平台的聚合式软件",
"link": "https://infogenie.shumengya.top",
"icon": "https://img.shumengya.top/i/2025/11/02/6907516fb77d5.png",
"tags": [ "聚合","工具"],
"admin":false,
"show":true,
"develop":true
},
{
"id": 9,
"title": "萌芽农场",
"description": "一款2D平台联机农场经营游戏",
"link": "https://work.shumengya.top/#/work/mengyafarm",
"icon": "https://img.shumengya.top/i/2025/11/02/6907599cbaf10.png",
"tags": [ "农场","游戏","联机"],
"admin":false,
"show":true,
"develop":true
},
{
"id": 10,
"title": "1Panel面板",
"description": "大萌芽1panel面板后台",
"link": "https://1panel.shumengya.top",
"icon": "https://img.shumengya.top/i/2025/11/02/69076213d9200.webp",
"tags": [ "1Panel","面板"],
"admin":true,
"show":true,
"develop":false
},
{
"id": 11,
"title": "DPanel面板",
"description": "大萌芽dpanel面板后台",
"link": "https://dpanel.shumengya.top",
"icon": "https://img.shumengya.top/i/2025/11/02/6907621448bac.png",
"tags": [ "Docker","面板"],
"admin":true,
"show":true,
"develop":false
},
{
"id": 12,
"title": "Frps管理后台",
"description": "成都公网内网穿透服务端",
"link": "https://frps.shumengya.top",
"icon": "https://img.shumengya.top/i/2025/11/02/6907621475130.png",
"tags": [ "内网穿透","Frp"],
"admin":true,
"show":true,
"develop":false
},
{
"id": 13,
"title": "Frpc管理后台",
"description": "大萌芽内网穿透客户端",
"link": "https://frpc.shumengya.top",
"icon": "https://img.shumengya.top/i/2025/11/02/6907621475130.png",
"tags": [ "内网穿透","Frp"],
"admin":true,
"show":true,
"develop":false
},
{
"id": 13,
"title": "萌芽问卷",
"description": "一个轻量简单的问卷系统",
"link": "https://survey.shumengya.top",
"icon": "https://img.shumengya.top/i/2025/11/02/690762865166c.png",
"tags": [ "问卷","调查","SurveyKing"],
"admin":false,
"show":true,
"develop":false
},
{
"id": 14,
"title": "HeadScale管理后台",
"description": "一个自建tailscale管理后台",
"link": "https://headscale.shumengya.top",
"icon": "https://img.shumengya.top/i/2025/11/03/6908327149dce.png",
"tags": [ "TailScale","HeadScale"],
"admin":true,
"show":true,
"develop":false
}
]
}

View File

@@ -0,0 +1,115 @@
{
"title": "技术栈",
"items": [
{
"name": "Python",
"icon": "https://img.shields.io/badge/-Python-3776AB?style=flat&logo=python&logoColor=white",
"link": "https://www.python.org/"
},
{
"name": "JavaScript",
"icon": "https://img.shields.io/badge/-JavaScript-F7DF1E?style=flat&logo=javascript&logoColor=black",
"link": "https://developer.mozilla.org/en-US/docs/Web/JavaScript"
},
{
"name": "Java",
"icon": "https://img.shields.io/badge/-Java-007396?style=flat&logo=java&logoColor=white",
"link": "https://www.oracle.com/java/"
},
{
"name": "C#",
"icon": "https://img.shields.io/badge/-C%23-512BD4?style=flat&logo=csharp&logoColor=white",
"link": "https://learn.microsoft.com/en-us/dotnet/csharp/"
},
{
"name": "Golang",
"icon": "https://img.shields.io/badge/-Golang-00ADD8?style=flat&logo=go&logoColor=white",
"link": "https://go.dev/"
},
{
"name": "React",
"icon": "https://img.shields.io/badge/-React-20232A?style=flat&logo=react&logoColor=61DAFB",
"link": "https://react.dev/"
},
{
"name": "Node.js",
"icon": "https://img.shields.io/badge/-Node.js-339933?style=flat&logo=nodedotjs&logoColor=white",
"link": "https://nodejs.org/"
},
{
"name": "Docker",
"icon": "https://img.shields.io/badge/-Docker-2496ED?style=flat&logo=docker&logoColor=white",
"link": "https://www.docker.com/"
},
{
"name": "Linux",
"icon": "https://img.shields.io/badge/-Linux-000000?style=flat&logo=linux&logoColor=white",
"link": "https://www.linux.org/"
},
{
"name": "Git",
"icon": "https://img.shields.io/badge/-Git-F05032?style=flat&logo=git&logoColor=white",
"link": "https://git-scm.com/"
},
{
"name": "Flask",
"icon": "https://img.shields.io/badge/-Flask-000000?style=flat&logo=flask&logoColor=white",
"link": "https://flask.palletsprojects.com/"
},
{
"name": "MongoDB",
"icon": "https://img.shields.io/badge/-MongoDB-47A248?style=flat&logo=mongodb&logoColor=white",
"link": "https://www.mongodb.com/"
},
{
"name": "PostgreSQL",
"icon": "https://img.shields.io/badge/-PostgreSQL-4169E1?style=flat&logo=postgresql&logoColor=white",
"link": "https://www.postgresql.org/"
},
{
"name": "MySQL",
"icon": "https://img.shields.io/badge/-MySQL-00758F?style=flat&logo=mysql&logoColor=white",
"link": "https://www.mysql.com/"
},
{
"name": "Redis",
"icon": "https://img.shields.io/badge/-Redis-DC382D?style=flat&logo=redis&logoColor=white",
"link": "https://redis.io/"
},
{
"name": "Android",
"icon": "https://img.shields.io/badge/-Android-3DDC84?style=flat&logo=android&logoColor=white",
"link": "https://developer.android.com/"
},
{
"name": "Flutter",
"icon": "https://img.shields.io/badge/-Flutter-02569B?style=flat&logo=flutter&logoColor=white",
"link": "https://flutter.dev/"
},
{
"name": "Godot",
"icon": "https://img.shields.io/badge/-Godot-478CBF?style=flat&logo=godot&logoColor=white",
"link": "https://godotengine.org/"
},
{
"name": "Unity",
"icon": "https://img.shields.io/badge/-Unity-000000?style=flat&logo=unity&logoColor=white",
"link": "https://unity.com/"
},
{
"name": "JSON",
"icon": "https://img.shields.io/badge/-JSON-000000?style=flat&logo=json&logoColor=white",
"link": "https://www.json.org/"
},
{
"name": "Markdown",
"icon": "https://img.shields.io/badge/-Markdown-083fa1?style=flat&logo=markdown&logoColor=white",
"link": "https://daringfireball.net/projects/markdown/"
},
{
"name": "Minecraft",
"icon": "https://img.shields.io/badge/-Minecraft-62B34A?style=flat&logo=minecraft&logoColor=white",
"link": "https://www.minecraft.net/"
}
]
}

View File

@@ -0,0 +1,120 @@
{
"title": "技术栈",
"items": [
{
"name": "Python",
"icon": "https://img.shields.io/badge/-Python-3776AB?style=flat&logo=python&logoColor=white",
"link": "https://www.python.org/"
},
{
"name": "JavaScript",
"icon": "https://img.shields.io/badge/-JavaScript-F7DF1E?style=flat&logo=javascript&logoColor=black",
"link": ""
},
{
"name": "Java",
"icon": "https://img.shields.io/badge/-Java-007396?style=flat&logo=java&logoColor=white",
"link": ""
},
{
"name": "C#",
"icon": "https://img.shields.io/badge/-C%23-512BD4?style=flat&logo=csharp&logoColor=white",
"link": ""
},
{
"name": "Golang",
"icon": "https://img.shields.io/badge/-Golang-00ADD8?style=flat&logo=go&logoColor=white",
"link": ""
},
{
"name": "React",
"icon": "https://img.shields.io/badge/-React-20232A?style=flat&logo=react&logoColor=61DAFB",
"link": ""
},
{
"name": "Node.js",
"icon": "https://img.shields.io/badge/-Node.js-339933?style=flat&logo=nodedotjs&logoColor=white",
"link": ""
},
{
"name": "Docker",
"icon": "https://img.shields.io/badge/-Docker-2496ED?style=flat&logo=docker&logoColor=white",
"link": ""
},
{
"name": "Linux",
"icon": "https://img.shields.io/badge/-Linux-000000?style=flat&logo=linux&logoColor=white",
"link": ""
},
{
"name": "Git",
"icon": "https://img.shields.io/badge/-Git-F05032?style=flat&logo=git&logoColor=white",
"link": ""
},
{
"name": "Flask",
"icon": "https://img.shields.io/badge/-Flask-000000?style=flat&logo=flask&logoColor=white",
"link": ""
},
{
"name": "MongoDB",
"icon": "https://img.shields.io/badge/-MongoDB-47A248?style=flat&logo=mongodb&logoColor=white",
"link": ""
},
{
"name": "PostgreSQL",
"icon": "https://img.shields.io/badge/-PostgreSQL-4169E1?style=flat&logo=postgresql&logoColor=white",
"link": ""
},
{
"name": "MySQL",
"icon": "https://img.shields.io/badge/-MySQL-00758F?style=flat&logo=mysql&logoColor=white",
"link": ""
},
{
"name": "Redis",
"icon": "https://img.shields.io/badge/-Redis-DC382D?style=flat&logo=redis&logoColor=white",
"link": ""
},
{
"name": "Android",
"icon": "https://img.shields.io/badge/-Android-3DDC84?style=flat&logo=android&logoColor=white",
"link": ""
},
{
"name": "Flutter",
"icon": "https://img.shields.io/badge/-Flutter-02569B?style=flat&logo=flutter&logoColor=white",
"link": ""
},
{
"name": "Godot",
"icon": "https://img.shields.io/badge/-Godot-478CBF?style=flat&logo=godot&logoColor=white",
"link": ""
},
{
"name": "Unity",
"icon": "https://img.shields.io/badge/-Unity-000000?style=flat&logo=unity&logoColor=white",
"link": ""
},
{
"name": "JSON",
"icon": "https://img.shields.io/badge/-JSON-000000?style=flat&logo=json&logoColor=white",
"link": ""
},
{
"name": "Markdown",
"icon": "https://img.shields.io/badge/-Markdown-083fa1?style=flat&logo=markdown&logoColor=white",
"link": ""
},
{
"name": "Minecraft",
"icon": "https://img.shields.io/badge/-Minecraft-62B34A?style=flat&logo=minecraft&logoColor=white",
"link": ""
}
]
}

View File

@@ -0,0 +1,2 @@
Flask==3.0.0
flask-cors==4.0.0

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';

42
start-backend.bat Normal file
View File

@@ -0,0 +1,42 @@
@echo off
setlocal
title Mengya Profile - Start Backend
echo [INFO] Starting backend API service...
cd /d "%~dp0mengyaprofile-backend"
REM Choose Python launcher: prefer py, else python
set "PY=python"
where py >nul 2>nul
if not errorlevel 1 set "PY=py"
where %PY% >nul 2>nul
if errorlevel 1 goto no_python
echo [INFO] Installing Python dependencies...
%PY% -m pip install -r requirements.txt
if errorlevel 1 goto pip_fail
echo [INFO] Starting in DEVELOPMENT mode...
echo [INFO] Frontend should run on http://localhost:3000
echo [INFO] Backend API on http://localhost:5000
set RUN_MODE=development
echo [INFO] Running: %PY% app.py
%PY% app.py
if errorlevel 1 goto run_fail
goto end
:no_python
echo [ERROR] Python not found. Install Python 3 and ensure PATH.
goto end
:pip_fail
echo [ERROR] pip install failed.
goto end
:run_fail
echo [ERROR] Backend failed to start.
goto end
:end
endlocal

8
start-dev-backend.bat Normal file
View File

@@ -0,0 +1,8 @@
@echo off
cd /d "%~dp0mengyaprofile-backend"
set RUN_MODE=development
echo Starting backend in DEVELOPMENT mode...
echo Backend API: http://localhost:5000
echo Frontend should run on: http://localhost:3000
echo.
python app.py

25
start-frontend.bat Normal file
View File

@@ -0,0 +1,25 @@
@echo off
setlocal
title Mengya Profile - Start Frontend
echo [INFO] 启动前端开发服务器...
cd /d "%~dp0mengyaprofile-frontend"
echo [INFO] 当前目录:%CD%
where npm >nul 2>nul
if errorlevel 1 (
echo [ERROR] 未检测到 npm请先安装 Node.js含 npm
exit /b 1
)
if not exist "node_modules" (
echo [INFO] 检测到缺少依赖,正在安装...
npm install --prefix "%CD%"
if errorlevel 1 (
echo [ERROR] 依赖安装失败。
exit /b 1
)
)
echo [INFO] 运行npm start
npm start --prefix "%CD%"
endlocal