chore: sync local changes (2026-03-12)
@@ -1,41 +0,0 @@
|
|||||||
# 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
|
|
||||||
47
AGENTS.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
|
||||||
|
- `mengyaprofile-frontend/`: React (Create React App) UI (`src/`, `public/`).
|
||||||
|
- `mengyaprofile-backend/`: Flask API (`app.py`) plus site content in `data/*.json` and assets in `data/logo/`, `data/background/`.
|
||||||
|
- Root `*.bat`: Windows helper scripts for starting/building locally.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend (Flask API)
|
||||||
|
cd mengyaprofile-backend
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
python app.py # http://localhost:5000
|
||||||
|
|
||||||
|
# Frontend (React)
|
||||||
|
cd ../mengyaprofile-frontend
|
||||||
|
npm install
|
||||||
|
npm start # http://localhost:3000
|
||||||
|
npm test # Jest/RTL in watch mode
|
||||||
|
npm run build # production build to ./build
|
||||||
|
```
|
||||||
|
|
||||||
|
- Windows shortcuts: `start-backend.bat`, `start-frontend.bat`, `build-frontend.bat`.
|
||||||
|
- Docker (optional): `docker compose -f mengyaprofile-backend/docker-compose.yml up -d --build` (adjust the volume path for your machine).
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
|
||||||
|
- Python: PEP 8, 4-space indents; keep API routes under `/api/*` in `mengyaprofile-backend/app.py`.
|
||||||
|
- React: 2-space indents; components live in `mengyaprofile-frontend/src/components/` with `PascalCase` filenames (e.g., `TechStackSection.js`).
|
||||||
|
- Data files: edit `mengyaprofile-backend/data/*.json` (UTF-8). Prefer stable keys and keep lists ordered to produce readable diffs.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
- Frontend tests live in `mengyaprofile-frontend/src/**/*.test.js` (example: `src/App.test.js`); run via `npm test`.
|
||||||
|
- Backend currently has no test suite; if adding one, use `pytest` and place tests under `mengyaprofile-backend/tests/`.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
|
- Current Git history uses short subjects (e.g., “Initial commit”, “初始化提交”); keep messages concise and scoped (`frontend: ...`, `backend: ...`).
|
||||||
|
- PRs: describe behavior changes, link issues, include screenshots for UI changes, and call out any `data/*.json` schema updates.
|
||||||
|
|
||||||
|
## Security & Configuration Tips
|
||||||
|
|
||||||
|
- “Admin mode” is client-side (`/admin?token=...`) and not a security boundary—do not store secrets in this repo.
|
||||||
|
- Useful env vars: backend `RUN_MODE`, `DATA_DIR`, `BACKGROUND_DIR`, `PORT`; frontend `REACT_APP_API_URL` (use `.env.local`).
|
||||||
42
Dockerfile
@@ -1,42 +0,0 @@
|
|||||||
# 多阶段构建 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"]
|
|
||||||
@@ -32,7 +32,7 @@ docker-compose up -d --build
|
|||||||
|
|
||||||
## 项目简介
|
## 项目简介
|
||||||
|
|
||||||
这是一个功能完整、设计精美的个人主页系统,展示个人信息、精选项目和联系方式。
|
这是一个功能完整、设计精美的个人主页系统,展示个人信息、全部项目和联系方式。
|
||||||
|
|
||||||
### 特性亮点
|
### 特性亮点
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ docker-compose up -d --build
|
|||||||
- 🐳 **Docker 支持**: 一键部署,开箱即用
|
- 🐳 **Docker 支持**: 一键部署,开箱即用
|
||||||
- 💾 **数据持久化**: 配置文件外部存储
|
- 💾 **数据持久化**: 配置文件外部存储
|
||||||
- ⚡ **快速灵活**: 通过 JSON 配置文件轻松管理内容
|
- ⚡ **快速灵活**: 通过 JSON 配置文件轻松管理内容
|
||||||
- 🎯 **三大模块**: 个人信息、精选项目、联系方式
|
- 🎯 **三大模块**: 个人信息、全部项目、联系方式
|
||||||
- 🔐 **权限控制**: 管理员模式隐藏私密项目
|
- 🔐 **权限控制**: 管理员模式隐藏私密项目
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
@@ -124,7 +124,7 @@ npm start
|
|||||||
|
|
||||||
**配置文件**: `mengyaprofile-backend/data/profile.json`
|
**配置文件**: `mengyaprofile-backend/data/profile.json`
|
||||||
|
|
||||||
### 2️⃣ 精选项目模块
|
### 2️⃣ 全部项目模块
|
||||||
|
|
||||||
以卡片形式展示项目:
|
以卡片形式展示项目:
|
||||||
- 📦 项目标题
|
- 📦 项目标题
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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
@@ -1,176 +0,0 @@
|
|||||||
#!/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
|
|
||||||
18
mengyaprofile-backend/.dockerignore
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.so
|
||||||
|
*.egg
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
28
mengyaprofile-backend/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 使用 Python 官方镜像
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV RUN_MODE=production
|
||||||
|
ENV DATA_DIR=/app/data
|
||||||
|
|
||||||
|
# 复制依赖文件
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# 复制应用代码
|
||||||
|
COPY app.py .
|
||||||
|
|
||||||
|
# 创建数据目录(如果挂载了外部卷,这个目录会被覆盖)
|
||||||
|
RUN mkdir -p /app/data/logo
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
CMD ["python", "app.py"]
|
||||||
@@ -19,7 +19,7 @@ python app.py
|
|||||||
## API 接口
|
## API 接口
|
||||||
|
|
||||||
- `GET /api/profile` - 获取个人基本信息
|
- `GET /api/profile` - 获取个人基本信息
|
||||||
- `GET /api/projects` - 获取精选项目列表
|
- `GET /api/projects` - 获取全部项目列表
|
||||||
- `GET /api/contacts` - 获取联系方式
|
- `GET /api/contacts` - 获取联系方式
|
||||||
- `GET /api/all` - 获取所有数据
|
- `GET /api/all` - 获取所有数据
|
||||||
|
|
||||||
|
|||||||
@@ -7,22 +7,27 @@ import random
|
|||||||
# 检测运行模式:通过环境变量控制
|
# 检测运行模式:通过环境变量控制
|
||||||
RUN_MODE = os.environ.get('RUN_MODE', 'development') # development 或 production
|
RUN_MODE = os.environ.get('RUN_MODE', 'development') # development 或 production
|
||||||
|
|
||||||
|
# 数据文件路径 - 支持环境变量配置(需要先定义,因为后面会用到)
|
||||||
|
DATA_DIR = os.environ.get('DATA_DIR', os.path.join(os.path.dirname(__file__), 'data'))
|
||||||
|
|
||||||
# 根据运行模式配置
|
# 根据运行模式配置
|
||||||
if RUN_MODE == 'production':
|
# 检查是否有前端构建文件(前后端分离时可能没有)
|
||||||
# 生产环境:使用构建后的前端
|
FRONTEND_BUILD_PATH = os.path.join(os.path.dirname(__file__), '..', 'frontend', 'build')
|
||||||
FRONTEND_BUILD_PATH = os.path.join(os.path.dirname(__file__), '..', 'frontend', 'build')
|
HAS_FRONTEND_BUILD = os.path.exists(FRONTEND_BUILD_PATH) and os.path.isdir(FRONTEND_BUILD_PATH)
|
||||||
|
|
||||||
|
# 背景图片目录 - 固定使用数据目录中的 background 文件夹
|
||||||
|
# 支持通过环境变量配置,默认在数据目录中
|
||||||
|
BACKGROUND_DIR = os.environ.get('BACKGROUND_DIR', os.path.join(DATA_DIR, 'background'))
|
||||||
|
|
||||||
|
if RUN_MODE == 'production' and HAS_FRONTEND_BUILD:
|
||||||
|
# 生产环境:使用构建后的前端(如果存在)
|
||||||
app = Flask(__name__, static_folder=FRONTEND_BUILD_PATH, static_url_path='')
|
app = Flask(__name__, static_folder=FRONTEND_BUILD_PATH, static_url_path='')
|
||||||
BACKGROUND_DIR = os.path.join(FRONTEND_BUILD_PATH, 'background')
|
|
||||||
else:
|
else:
|
||||||
# 开发环境:不服务前端,只提供 API
|
# 开发环境或纯后端模式:只提供 API
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
BACKGROUND_DIR = os.path.join(os.path.dirname(__file__), '..', 'mengyaprofile-frontend', 'public', 'background')
|
|
||||||
|
|
||||||
CORS(app) # 允许跨域请求
|
CORS(app) # 允许跨域请求
|
||||||
|
|
||||||
# 数据文件路径 - 支持环境变量配置
|
|
||||||
DATA_DIR = os.environ.get('DATA_DIR', os.path.join(os.path.dirname(__file__), 'data'))
|
|
||||||
|
|
||||||
def load_json_file(filename):
|
def load_json_file(filename):
|
||||||
"""加载JSON文件"""
|
"""加载JSON文件"""
|
||||||
try:
|
try:
|
||||||
@@ -44,7 +49,7 @@ def get_profile():
|
|||||||
|
|
||||||
@app.route('/api/projects', methods=['GET'])
|
@app.route('/api/projects', methods=['GET'])
|
||||||
def get_projects():
|
def get_projects():
|
||||||
"""获取精选项目列表"""
|
"""获取全部项目列表"""
|
||||||
data = load_json_file('projects.json')
|
data = load_json_file('projects.json')
|
||||||
if data:
|
if data:
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
@@ -66,22 +71,78 @@ def get_techstack():
|
|||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
return jsonify({"error": "Tech stack没有找到"}), 404
|
return jsonify({"error": "Tech stack没有找到"}), 404
|
||||||
|
|
||||||
|
@app.route('/api/logo/<filename>', methods=['GET'])
|
||||||
|
def get_logo(filename):
|
||||||
|
"""提供技术栈图标文件"""
|
||||||
|
logo_dir = os.path.join(DATA_DIR, 'logo')
|
||||||
|
try:
|
||||||
|
# 安全检查:防止路径遍历攻击
|
||||||
|
if '..' in filename or '/' in filename or '\\' in filename:
|
||||||
|
return jsonify({"error": "无效的文件名"}), 400
|
||||||
|
|
||||||
|
# 检查文件是否存在
|
||||||
|
file_path = os.path.join(logo_dir, filename)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
print(f"图标文件不存在: {file_path}")
|
||||||
|
return jsonify({"error": f"图标文件未找到: {filename}"}), 404
|
||||||
|
|
||||||
|
# 检查目录是否存在
|
||||||
|
if not os.path.exists(logo_dir):
|
||||||
|
print(f"图标目录不存在: {logo_dir}")
|
||||||
|
return jsonify({"error": "图标目录未找到"}), 404
|
||||||
|
|
||||||
|
return send_from_directory(logo_dir, filename)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取图标文件出错: {e}")
|
||||||
|
print(f"尝试访问的文件: {os.path.join(logo_dir, filename)}")
|
||||||
|
return jsonify({"error": f"图标文件未找到: {filename}"}), 404
|
||||||
|
|
||||||
@app.route('/api/random-background', methods=['GET'])
|
@app.route('/api/random-background', methods=['GET'])
|
||||||
def get_random_background():
|
def get_random_background():
|
||||||
"""获取随机背景图片"""
|
"""获取随机背景图片"""
|
||||||
try:
|
try:
|
||||||
# 获取背景图片目录中的所有图片
|
# 获取背景图片目录中的所有图片
|
||||||
if os.path.exists(BACKGROUND_DIR):
|
if os.path.exists(BACKGROUND_DIR) and os.path.isdir(BACKGROUND_DIR):
|
||||||
images = [f for f in os.listdir(BACKGROUND_DIR)
|
images = [f for f in os.listdir(BACKGROUND_DIR)
|
||||||
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.gif'))]
|
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.gif'))]
|
||||||
if images:
|
if images:
|
||||||
random_image = random.choice(images)
|
random_image = random.choice(images)
|
||||||
return jsonify({"image": f"/background/{random_image}"})
|
# 返回完整的 API 路径
|
||||||
|
return jsonify({"image": f"/api/background/{random_image}"})
|
||||||
|
else:
|
||||||
|
print(f"背景图片目录不存在: {BACKGROUND_DIR}")
|
||||||
return jsonify({"image": None})
|
return jsonify({"image": None})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"获取随机背景出错: {e}")
|
print(f"获取随机背景出错: {e}")
|
||||||
|
print(f"背景目录路径: {BACKGROUND_DIR}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
return jsonify({"image": None})
|
return jsonify({"image": None})
|
||||||
|
|
||||||
|
@app.route('/api/background/<filename>', methods=['GET'])
|
||||||
|
def get_background_image(filename):
|
||||||
|
"""提供背景图片文件"""
|
||||||
|
try:
|
||||||
|
# 安全检查:防止路径遍历攻击
|
||||||
|
if '..' in filename or '/' in filename or '\\' in filename:
|
||||||
|
return jsonify({"error": "无效的文件名"}), 400
|
||||||
|
|
||||||
|
# 检查目录是否存在
|
||||||
|
if not os.path.exists(BACKGROUND_DIR):
|
||||||
|
print(f"背景图片目录不存在: {BACKGROUND_DIR}")
|
||||||
|
return jsonify({"error": "背景图片目录未找到"}), 404
|
||||||
|
|
||||||
|
# 检查文件是否存在
|
||||||
|
file_path = os.path.join(BACKGROUND_DIR, filename)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
print(f"背景图片文件不存在: {file_path}")
|
||||||
|
return jsonify({"error": f"背景图片未找到: {filename}"}), 404
|
||||||
|
|
||||||
|
return send_from_directory(BACKGROUND_DIR, filename)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取背景图片出错: {e}")
|
||||||
|
return jsonify({"error": f"背景图片未找到: {filename}"}), 404
|
||||||
|
|
||||||
@app.route('/api/all', methods=['GET'])
|
@app.route('/api/all', methods=['GET'])
|
||||||
def get_all():
|
def get_all():
|
||||||
"""获取所有数据"""
|
"""获取所有数据"""
|
||||||
@@ -100,37 +161,45 @@ def get_all():
|
|||||||
@app.route('/', methods=['GET'])
|
@app.route('/', methods=['GET'])
|
||||||
def index():
|
def index():
|
||||||
"""服务前端页面或API信息"""
|
"""服务前端页面或API信息"""
|
||||||
if RUN_MODE == 'production' and app.static_folder:
|
if RUN_MODE == 'production' and app.static_folder and os.path.exists(os.path.join(app.static_folder, 'index.html')):
|
||||||
# 生产环境,返回前端页面
|
# 生产环境,返回前端页面(如果存在)
|
||||||
return send_from_directory(app.static_folder, 'index.html')
|
try:
|
||||||
else:
|
return send_from_directory(app.static_folder, 'index.html')
|
||||||
# 开发环境,返回API信息
|
except:
|
||||||
return jsonify({
|
pass
|
||||||
"message": "萌芽主页 后端API - 开发模式",
|
|
||||||
"author": "树萌芽",
|
# 返回API信息
|
||||||
"version": "1.0.0",
|
return jsonify({
|
||||||
"mode": "development",
|
"message": "萌芽主页 后端API",
|
||||||
"note": "前端开发服务器运行在 http://localhost:3000",
|
"author": "树萌芽",
|
||||||
"endpoints": {
|
"version": "1.0.0",
|
||||||
"/api/profile": "获取个人信息",
|
"mode": RUN_MODE,
|
||||||
"/api/techstack": "获取技术栈",
|
"note": "这是一个纯后端API服务,前端请访问独立的前端应用",
|
||||||
"/api/projects": "获取项目列表",
|
"api_base": "https://nav.api.shumengya.top/api",
|
||||||
"/api/contacts": "获取联系方式",
|
"endpoints": {
|
||||||
"/api/random-background": "获取随机背景图片",
|
"/api/profile": "获取个人信息",
|
||||||
"/api/all": "获取所有数据"
|
"/api/techstack": "获取技术栈",
|
||||||
}
|
"/api/projects": "获取项目列表",
|
||||||
})
|
"/api/contacts": "获取联系方式",
|
||||||
|
"/api/random-background": "获取随机背景图片",
|
||||||
|
"/api/all": "获取所有数据"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
@app.route('/admin')
|
@app.route('/admin')
|
||||||
def admin():
|
def admin():
|
||||||
"""服务管理员页面(也是前端)"""
|
"""服务管理员页面(也是前端)"""
|
||||||
if RUN_MODE == 'production' and app.static_folder:
|
if RUN_MODE == 'production' and app.static_folder and os.path.exists(os.path.join(app.static_folder, 'index.html')):
|
||||||
return send_from_directory(app.static_folder, 'index.html')
|
try:
|
||||||
else:
|
return send_from_directory(app.static_folder, 'index.html')
|
||||||
return jsonify({
|
except:
|
||||||
"error": "开发模式",
|
pass
|
||||||
"note": "请访问 http://localhost:3000/admin?token=shumengya520"
|
|
||||||
}), 404
|
return jsonify({
|
||||||
|
"error": "管理员页面未找到",
|
||||||
|
"note": "这是一个纯后端API服务,请访问独立的前端应用",
|
||||||
|
"api_base": "https://nav.api.shumengya.top/api"
|
||||||
|
}), 404
|
||||||
|
|
||||||
@app.route('/api')
|
@app.route('/api')
|
||||||
def api_info():
|
def api_info():
|
||||||
@@ -149,7 +218,7 @@ def api_info():
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# 处理前端路由 - 所有非API请求都返回 index.html
|
# 处理404错误
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
"""处理404错误"""
|
"""处理404错误"""
|
||||||
@@ -157,17 +226,32 @@ def not_found(e):
|
|||||||
if request.path.startswith('/api'):
|
if request.path.startswith('/api'):
|
||||||
return jsonify({"error": "API endpoint not found"}), 404
|
return jsonify({"error": "API endpoint not found"}), 404
|
||||||
|
|
||||||
# 非API请求
|
# 非API请求 - 如果是前后端分离,返回API信息
|
||||||
if RUN_MODE == 'production' and app.static_folder:
|
if RUN_MODE == 'production' and app.static_folder and os.path.exists(os.path.join(app.static_folder, 'index.html')):
|
||||||
# 生产环境返回前端页面(支持前端路由)
|
# 如果有前端构建文件,尝试返回
|
||||||
return send_from_directory(app.static_folder, 'index.html')
|
try:
|
||||||
|
return send_from_directory(app.static_folder, 'index.html')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# 开发环境
|
# 返回API信息
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"error": "页面未找到",
|
"error": "页面未找到",
|
||||||
"mode": "development",
|
"message": "这是一个纯后端API服务",
|
||||||
"note": "开发环境请访问 http://localhost:3000"
|
"api_base": "https://nav.api.shumengya.top/api",
|
||||||
|
"endpoints": {
|
||||||
|
"/api/profile": "获取个人信息",
|
||||||
|
"/api/techstack": "获取技术栈",
|
||||||
|
"/api/projects": "获取项目列表",
|
||||||
|
"/api/contacts": "获取联系方式",
|
||||||
|
"/api/random-background": "获取随机背景图片",
|
||||||
|
"/api/all": "获取所有数据"
|
||||||
|
}
|
||||||
}), 404
|
}), 404
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
# 从环境变量获取端口,默认为 5000
|
||||||
|
port = int(os.environ.get('PORT', 5000))
|
||||||
|
# 生产环境关闭 debug 模式
|
||||||
|
debug_mode = RUN_MODE != 'production'
|
||||||
|
app.run(debug=debug_mode, host='0.0.0.0', port=port)
|
||||||
|
|||||||
22
mengyaprofile-backend/docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mengyaprofile-backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: mengyaprofile-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "1616:5000"
|
||||||
|
volumes:
|
||||||
|
- /shumengya/docker/mengyaprofile-backend/data:/app/data
|
||||||
|
environment:
|
||||||
|
- RUN_MODE=production
|
||||||
|
- DATA_DIR=/app/data
|
||||||
|
networks:
|
||||||
|
- mengyaprofile-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mengyaprofile-network:
|
||||||
|
driver: bridge
|
||||||
59
mengyaprofile-backend/sort_techstack.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
技术栈排序和更新脚本
|
||||||
|
- 按照技术栈名称的首字母排序
|
||||||
|
- 添加新的技术栈项
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
def sort_and_update_techstack():
|
||||||
|
# 文件路径
|
||||||
|
file_path = os.path.join(os.path.dirname(__file__), 'data', 'techstack.json')
|
||||||
|
|
||||||
|
# 读取JSON文件
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# 新添加的技术栈项
|
||||||
|
new_items = [
|
||||||
|
{
|
||||||
|
"name": "Spring",
|
||||||
|
"icon": "https://img.shields.io/badge/-Spring-6DB33F?style=flat&logo=spring&logoColor=white",
|
||||||
|
"link": "https://spring.io/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gin",
|
||||||
|
"icon": "https://img.shields.io/badge/-Gin-00ADD8?style=flat&logo=go&logoColor=white",
|
||||||
|
"link": "https://gin-gonic.com/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 获取现有项的名称集合,用于检查是否已存在
|
||||||
|
existing_names = {item['name'] for item in data['items']}
|
||||||
|
|
||||||
|
# 添加新项(如果不存在)
|
||||||
|
for new_item in new_items:
|
||||||
|
if new_item['name'] not in existing_names:
|
||||||
|
data['items'].append(new_item)
|
||||||
|
print(f"已添加: {new_item['name']}")
|
||||||
|
else:
|
||||||
|
print(f"已存在,跳过: {new_item['name']}")
|
||||||
|
|
||||||
|
# 按照名称的首字母排序(不区分大小写)
|
||||||
|
data['items'].sort(key=lambda x: x['name'].upper())
|
||||||
|
|
||||||
|
# 写回文件,保持格式美观
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print(f"\n排序完成!共 {len(data['items'])} 个技术栈项")
|
||||||
|
print("技术栈列表(按首字母排序):")
|
||||||
|
for i, item in enumerate(data['items'], 1):
|
||||||
|
print(f" {i}. {item['name']}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sort_and_update_techstack()
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
- 技术定位
|
- 技术定位
|
||||||
- 个人座右铭
|
- 个人座右铭
|
||||||
|
|
||||||
### 2. 精选项目模块
|
### 2. 全部项目模块
|
||||||
以卡片形式展示项目:
|
以卡片形式展示项目:
|
||||||
- 项目标题
|
- 项目标题
|
||||||
- 项目简介
|
- 项目简介
|
||||||
|
|||||||
23
mengyaprofile-frontend/package-lock.json
generated
@@ -64,7 +64,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -714,7 +713,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz",
|
||||||
"integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==",
|
"integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1598,7 +1596,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz",
|
||||||
"integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
|
"integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
"@babel/helper-annotate-as-pure": "^7.27.1",
|
||||||
"@babel/helper-module-imports": "^7.27.1",
|
"@babel/helper-module-imports": "^7.27.1",
|
||||||
@@ -3428,7 +3425,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.10.4",
|
"@babel/code-frame": "^7.10.4",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
@@ -3915,7 +3911,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
||||||
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
|
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.4.0",
|
"@eslint-community/regexpp": "^4.4.0",
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
"@typescript-eslint/scope-manager": "5.62.0",
|
||||||
@@ -3969,7 +3964,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
|
||||||
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
"@typescript-eslint/scope-manager": "5.62.0",
|
||||||
"@typescript-eslint/types": "5.62.0",
|
"@typescript-eslint/types": "5.62.0",
|
||||||
@@ -4339,7 +4333,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -4438,7 +4431,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -5349,7 +5341,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.19",
|
"baseline-browser-mapping": "^2.8.19",
|
||||||
"caniuse-lite": "^1.0.30001751",
|
"caniuse-lite": "^1.0.30001751",
|
||||||
@@ -7189,7 +7180,6 @@
|
|||||||
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
|
||||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@@ -9966,7 +9956,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
|
||||||
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
|
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "^27.5.1",
|
"@jest/core": "^27.5.1",
|
||||||
"import-local": "^3.0.2",
|
"import-local": "^3.0.2",
|
||||||
@@ -10864,7 +10853,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@@ -12248,7 +12236,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -13383,7 +13370,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
"util-deprecate": "^1.0.2"
|
"util-deprecate": "^1.0.2"
|
||||||
@@ -13743,7 +13729,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -13875,7 +13860,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -13900,7 +13884,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||||
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
|
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -14347,7 +14330,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
||||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
@@ -14590,7 +14572,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -16264,7 +16245,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||||
"license": "(MIT OR CC0-1.0)",
|
"license": "(MIT OR CC0-1.0)",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -16694,7 +16674,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
|
||||||
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
|
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.8",
|
"@types/estree": "^1.0.8",
|
||||||
@@ -16766,7 +16745,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
|
||||||
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
|
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/bonjour": "^3.5.9",
|
"@types/bonjour": "^3.5.9",
|
||||||
"@types/connect-history-api-fallback": "^1.3.5",
|
"@types/connect-history-api-fallback": "^1.3.5",
|
||||||
@@ -17179,7 +17157,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "react-scripts start",
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.0 MiB |
|
Before Width: | Height: | Size: 6.5 MiB |
|
Before Width: | Height: | Size: 6.6 MiB |
|
Before Width: | Height: | Size: 822 KiB |
|
Before Width: | Height: | Size: 3.7 MiB |
|
Before Width: | Height: | Size: 6.3 MiB |
|
Before Width: | Height: | Size: 977 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 25 KiB |
@@ -4,7 +4,10 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#667eea" />
|
<meta name="theme-color" content="#52b788" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="萌芽主页" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="萌芽主页 - Full-Stack / Backend / DevOps"
|
content="萌芽主页 - Full-Stack / Backend / DevOps"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,25 +1,32 @@
|
|||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "萌芽主页",
|
||||||
"name": "Create React App Sample",
|
"name": "萌芽主页",
|
||||||
|
"description": "萌芽个人主页 - 全栈 / 后端 / DevOps",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
"type": "image/x-icon"
|
"type": "image/x-icon",
|
||||||
|
"purpose": "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "logo192.png",
|
"src": "logo192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192",
|
||||||
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "logo512.png",
|
"src": "logo192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512"
|
"sizes": "512x512",
|
||||||
|
"purpose": "any maskable"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#000000",
|
"orientation": "portrait-primary",
|
||||||
"background_color": "#ffffff"
|
"theme_color": "#52b788",
|
||||||
|
"background_color": "#a8e6cf",
|
||||||
|
"categories": ["personalization", "productivity"],
|
||||||
|
"prefer_related_applications": false
|
||||||
}
|
}
|
||||||
|
|||||||
53
mengyaprofile-frontend/public/service-worker.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/* PWA Service Worker - 萌芽主页 */
|
||||||
|
const CACHE_NAME = 'mengyaprofile-v1';
|
||||||
|
const urlsToCache = [
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/manifest.json',
|
||||||
|
'/favicon.ico',
|
||||||
|
'/logo192.png'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then((cache) => cache.addAll(urlsToCache))
|
||||||
|
.then(() => self.skipWaiting())
|
||||||
|
.catch((err) => console.log('SW install cache addAll failed', err))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) =>
|
||||||
|
Promise.all(
|
||||||
|
cacheNames
|
||||||
|
.filter((name) => name !== CACHE_NAME)
|
||||||
|
.map((name) => caches.delete(name))
|
||||||
|
)
|
||||||
|
).then(() => self.clients.claim())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
const { request } = event;
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.origin !== location.origin) return;
|
||||||
|
if (request.mode === 'navigate') {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(request).catch(() => caches.match('/index.html'))
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(request).then((cached) =>
|
||||||
|
cached || fetch(request).then((response) => {
|
||||||
|
if (response.ok && response.type === 'basic') {
|
||||||
|
const clone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -102,6 +102,153 @@ body {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PWA 启动画面 */
|
||||||
|
.pwa-launch {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 40%, #ffd3b6 100%);
|
||||||
|
animation: pwaLaunchBgPulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-bg::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.4) 0%, transparent 45%),
|
||||||
|
radial-gradient(circle at 70% 70%, rgba(255, 255, 255, 0.3) 0%, transparent 45%);
|
||||||
|
animation: pwaLaunchShine 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pwaLaunchBgPulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.92; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pwaLaunchShine {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.8; transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-align: center;
|
||||||
|
animation: pwaLaunchFadeIn 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pwaLaunchFadeIn {
|
||||||
|
from { opacity: 0; transform: scale(0.9); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-logo-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: 0 auto 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-logo {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
|
border-radius: 22px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
animation: pwaLaunchLogoFloat 2.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pwaLaunchLogoFloat {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-8px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-ring {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
border: 2px solid rgba(82, 183, 136, 0.4);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-ring-1 {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
animation: pwaLaunchRing 2s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-ring-2 {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
animation: pwaLaunchRing 2s ease-out 0.3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-ring-3 {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
animation: pwaLaunchRing 2s ease-out 0.6s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pwaLaunchRing {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) scale(0.6);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) scale(1.4);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
margin: 0 0 8px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #52b788;
|
||||||
|
animation: pwaLaunchDot 1.2s ease-in-out infinite both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-launch-dot:nth-child(1) { animation-delay: 0s; }
|
||||||
|
.pwa-launch-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.pwa-launch-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes pwaLaunchDot {
|
||||||
|
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
|
||||||
|
40% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
/* 错误状态 */
|
/* 错误状态 */
|
||||||
.error-container {
|
.error-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -141,6 +288,21 @@ body {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-visitor {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-visitor span {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0, 0, 0, 0.12);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.container {
|
.container {
|
||||||
@@ -155,4 +317,3 @@ body {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ function App() {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [backgroundImage, setBackgroundImage] = useState(null);
|
const [backgroundImage, setBackgroundImage] = useState(null);
|
||||||
const [isAdminMode, setIsAdminMode] = useState(false);
|
const [isAdminMode, setIsAdminMode] = useState(false);
|
||||||
|
const [visitorInfo, setVisitorInfo] = useState(null);
|
||||||
|
const [visitorInfoLoading, setVisitorInfoLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 检查是否为 admin 模式
|
// 检查是否为 admin 模式
|
||||||
@@ -29,9 +31,11 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 从后端API获取所有数据
|
// 从后端API获取所有数据
|
||||||
// 开发环境使用完整URL,生产环境使用相对路径
|
// 使用环境变量配置的API地址,默认为 nav.api.shumengya.top
|
||||||
const apiBaseUrl = process.env.REACT_APP_API_URL ||
|
const apiBaseUrl = process.env.REACT_APP_API_URL ||
|
||||||
(process.env.NODE_ENV === 'development' ? 'http://localhost:5000/api' : '/api');
|
(process.env.NODE_ENV === 'development'
|
||||||
|
? 'http://localhost:5000/api'
|
||||||
|
: 'https://nav.api.shumengya.top/api');
|
||||||
fetch(`${apiBaseUrl}/all`)
|
fetch(`${apiBaseUrl}/all`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -42,16 +46,28 @@ function App() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
setData(data);
|
setData(data);
|
||||||
|
|
||||||
// 设置 favicon
|
// 设置 favicon:优先使用 cf-favicon API,失败则用后端返回的 logo
|
||||||
if (data.profile?.favicon) {
|
const applyFavicon = (url) => {
|
||||||
let faviconTag = document.querySelector('link[rel="icon"]');
|
let faviconTag = document.querySelector('link[rel="icon"]');
|
||||||
if (!faviconTag) {
|
if (!faviconTag) {
|
||||||
faviconTag = document.createElement('link');
|
faviconTag = document.createElement('link');
|
||||||
faviconTag.rel = 'icon';
|
faviconTag.rel = 'icon';
|
||||||
document.head.appendChild(faviconTag);
|
document.head.appendChild(faviconTag);
|
||||||
}
|
}
|
||||||
faviconTag.href = data.profile.favicon;
|
faviconTag.href = url;
|
||||||
}
|
};
|
||||||
|
const fallbackFavicon = data.profile?.favicon || '';
|
||||||
|
const siteUrl = data.profile?.site
|
||||||
|
|| data.profile?.homepage
|
||||||
|
|| (data.contacts?.contacts?.find((c) => c.type === 'personprofile')?.link)
|
||||||
|
|| (data.contacts?.contacts?.find((c) => c.link?.startsWith('https://'))?.link)
|
||||||
|
|| window.location.origin;
|
||||||
|
const apiFaviconUrl = `https://cf-favicon.pages.dev/api/favicon?url=${encodeURIComponent(siteUrl)}`;
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => applyFavicon(apiFaviconUrl);
|
||||||
|
img.onerror = () => { if (fallbackFavicon) applyFavicon(fallbackFavicon); };
|
||||||
|
img.src = apiFaviconUrl;
|
||||||
|
if (fallbackFavicon && !siteUrl) applyFavicon(fallbackFavicon);
|
||||||
|
|
||||||
// 如果启用了本地背景,则获取随机背景图
|
// 如果启用了本地背景,则获取随机背景图
|
||||||
if (data.profile?.showlocalbackground) {
|
if (data.profile?.showlocalbackground) {
|
||||||
@@ -59,7 +75,18 @@ function App() {
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(bgData => {
|
.then(bgData => {
|
||||||
if (bgData.image) {
|
if (bgData.image) {
|
||||||
setBackgroundImage(bgData.image);
|
// 如果返回的是相对路径(以 /api/ 开头),转换为完整的 API URL
|
||||||
|
let imageUrl = bgData.image;
|
||||||
|
if (imageUrl.startsWith('/api/')) {
|
||||||
|
// 相对路径,需要添加域名
|
||||||
|
const baseUrl = apiBaseUrl.replace('/api', '');
|
||||||
|
imageUrl = `${baseUrl}${imageUrl}`;
|
||||||
|
} else if (imageUrl.startsWith('/')) {
|
||||||
|
// 其他相对路径
|
||||||
|
const baseUrl = apiBaseUrl.replace('/api', '');
|
||||||
|
imageUrl = `${baseUrl}${imageUrl}`;
|
||||||
|
}
|
||||||
|
setBackgroundImage(imageUrl);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => console.error('获取背景图片失败:', err));
|
.catch(err => console.error('获取背景图片失败:', err));
|
||||||
@@ -74,11 +101,58 @@ function App() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
setVisitorInfoLoading(true);
|
||||||
|
|
||||||
|
fetch('https://cf-ip-geo.smyhub.com/api', {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
cache: 'no-store'
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((payload) => {
|
||||||
|
setVisitorInfo(payload);
|
||||||
|
setVisitorInfoLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err?.name === 'AbortError') return;
|
||||||
|
console.warn('获取访客 IP/地理位置失败:', err);
|
||||||
|
setVisitorInfo(null);
|
||||||
|
setVisitorInfoLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => controller.abort();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="loading-container">
|
<div className="pwa-launch">
|
||||||
<div className="loading-spinner"></div>
|
<div className="pwa-launch-bg" />
|
||||||
<p>加载中...</p>
|
<div className="pwa-launch-content">
|
||||||
|
<div className="pwa-launch-logo-wrap">
|
||||||
|
<img
|
||||||
|
src={`${process.env.PUBLIC_URL || ''}/logo192.png`}
|
||||||
|
alt="萌芽"
|
||||||
|
className="pwa-launch-logo"
|
||||||
|
/>
|
||||||
|
<div className="pwa-launch-ring pwa-launch-ring-1" />
|
||||||
|
<div className="pwa-launch-ring pwa-launch-ring-2" />
|
||||||
|
<div className="pwa-launch-ring pwa-launch-ring-3" />
|
||||||
|
</div>
|
||||||
|
<h1 className="pwa-launch-title">萌芽主页</h1>
|
||||||
|
<p className="pwa-launch-subtitle">加载中</p>
|
||||||
|
<div className="pwa-launch-dots">
|
||||||
|
<span className="pwa-launch-dot" />
|
||||||
|
<span className="pwa-launch-dot" />
|
||||||
|
<span className="pwa-launch-dot" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -131,7 +205,7 @@ function App() {
|
|||||||
{/* 技术栈模块 */}
|
{/* 技术栈模块 */}
|
||||||
{data.techstack && <TechStackSection techstack={data.techstack} />}
|
{data.techstack && <TechStackSection techstack={data.techstack} />}
|
||||||
|
|
||||||
{/* 精选项目模块 */}
|
{/* 全部项目模块 */}
|
||||||
{data.projects && <ProjectsSection projects={data.projects.projects} />}
|
{data.projects && <ProjectsSection projects={data.projects.projects} />}
|
||||||
|
|
||||||
{/* 联系方式模块 */}
|
{/* 联系方式模块 */}
|
||||||
@@ -141,6 +215,27 @@ function App() {
|
|||||||
{/* 页脚 */}
|
{/* 页脚 */}
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
<p><strong>{data.profile?.footer || '© 2025 萌芽个人主页. All rights reserved.'}</strong></p>
|
<p><strong>{data.profile?.footer || '© 2025 萌芽个人主页. All rights reserved.'}</strong></p>
|
||||||
|
<div className="footer-visitor">
|
||||||
|
{visitorInfoLoading ? (
|
||||||
|
<span>访客信息加载中</span>
|
||||||
|
) : visitorInfo?.ip ? (
|
||||||
|
<span>
|
||||||
|
访客 IP:{visitorInfo.ip}
|
||||||
|
{visitorInfo.geo ? (
|
||||||
|
<>
|
||||||
|
{' · '}
|
||||||
|
{[
|
||||||
|
visitorInfo.geo.country,
|
||||||
|
visitorInfo.geo.region,
|
||||||
|
visitorInfo.geo.city
|
||||||
|
].filter(Boolean).join(' · ')}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>访客信息获取失败</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,53 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
const mockAllData = {
|
||||||
render(<App />);
|
profile: {
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
nickname: 'Test User',
|
||||||
expect(linkElement).toBeInTheDocument();
|
avatar: null,
|
||||||
|
introduction: 'Hello',
|
||||||
|
showlocalbackground: false,
|
||||||
|
footer: '© Test Footer'
|
||||||
|
},
|
||||||
|
techstack: null,
|
||||||
|
projects: null,
|
||||||
|
contacts: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockVisitorInfo = {
|
||||||
|
ip: '66.90.99.202',
|
||||||
|
ipVersion: 'ipv4',
|
||||||
|
geo: { country: 'JP', region: 'Tokyo', city: 'Ebara' }
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(global, 'fetch').mockImplementation((url) => {
|
||||||
|
if (typeof url === 'string' && url.includes('/api/all')) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockAllData)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (url === 'https://cf-ip-geo.smyhub.com/api') {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockVisitorInfo)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders visitor ip and geo in footer', async () => {
|
||||||
|
render(<App />);
|
||||||
|
const visitorLine = await screen.findByText(/访客 IP:66\.90\.99\.202/i);
|
||||||
|
expect(visitorLine).toBeInTheDocument();
|
||||||
|
expect(visitorLine).toHaveTextContent('JP');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,11 +19,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 标题和按钮容器 */
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: white;
|
color: white;
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -40,9 +50,49 @@
|
|||||||
50% { transform: translateY(-10px); }
|
50% { transform: translateY(-10px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 分类按钮容器 */
|
||||||
|
.category-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类按钮 */
|
||||||
|
.category-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 18px;
|
||||||
|
border: 2px solid rgba(82, 183, 136, 0.3);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-btn:hover {
|
||||||
|
background: rgba(82, 183, 136, 0.2);
|
||||||
|
border-color: rgba(82, 183, 136, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(82, 183, 136, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-btn.active {
|
||||||
|
background: linear-gradient(135deg, #52b788, #95d5b2);
|
||||||
|
border-color: #52b788;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 16px rgba(82, 183, 136, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.projects-grid {
|
.projects-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
grid-auto-rows: auto;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,10 +233,28 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.category-buttons {
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-btn {
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.project-card {
|
.project-card {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
@@ -225,9 +293,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 769px) and (max-width: 1024px) {
|
/* 桌面端固定为5列 */
|
||||||
|
@media (min-width: 1441px) {
|
||||||
.projects-grid {
|
.projects-grid {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,3 +305,48 @@
|
|||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
.projects-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页指示器样式 */
|
||||||
|
.pagination-dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-dot:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-dot.active {
|
||||||
|
background: #52b788;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
box-shadow: 0 0 10px rgba(82, 183, 136, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-dot:focus {
|
||||||
|
outline: 2px solid rgba(82, 183, 136, 0.5);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import './ProjectsSection.css';
|
import './ProjectsSection.css';
|
||||||
|
|
||||||
function ProjectsSection({ projects }) {
|
function ProjectsSection({ projects }) {
|
||||||
const [hoveredId, setHoveredId] = useState(null);
|
const [hoveredIndex, setHoveredIndex] = useState(null);
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
|
const [category, setCategory] = useState('all'); // 'all', 'develop', 'deploy'
|
||||||
|
const [itemsPerPage, setItemsPerPage] = useState(15); // 默认桌面端 3行 × 5列 = 15个项目
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 检查 URL 参数
|
// 检查 URL 参数
|
||||||
@@ -15,20 +18,45 @@ function ProjectsSection({ projects }) {
|
|||||||
if (pathname.includes('/admin') && token === 'shumengya520') {
|
if (pathname.includes('/admin') && token === 'shumengya520') {
|
||||||
setIsAdmin(true);
|
setIsAdmin(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据屏幕宽度设置每页显示数量
|
||||||
|
const updateItemsPerPage = () => {
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
setItemsPerPage(8); // 移动端:4行 × 2列 = 8个项目
|
||||||
|
} else {
|
||||||
|
setItemsPerPage(15); // 桌面端:3行 × 5列 = 15个项目
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateItemsPerPage();
|
||||||
|
window.addEventListener('resize', updateItemsPerPage);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', updateItemsPerPage);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getFavicon = (url) => {
|
// 优先使用 cf-favicon API,失败时 onError 会切到 project.icon 或通用图标
|
||||||
|
const getProjectIconUrl = (link) => {
|
||||||
try {
|
try {
|
||||||
const domain = new URL(url).origin;
|
new URL(link);
|
||||||
return `${domain}/favicon.ico`;
|
return `https://cf-favicon.pages.dev/api/favicon?url=${encodeURIComponent(link)}`;
|
||||||
} catch {
|
} catch {
|
||||||
return 'https://api.iconify.design/mdi:web.svg';
|
return 'https://api.iconify.design/mdi:web.svg';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleProjectIconError = (e, project) => {
|
||||||
|
const isApiUrl = e.target.src && e.target.src.includes('cf-favicon.pages.dev');
|
||||||
|
if (isApiUrl && project.icon) {
|
||||||
|
e.target.src = project.icon;
|
||||||
|
} else {
|
||||||
|
e.target.src = 'https://api.iconify.design/mdi:web.svg';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 过滤项目
|
// 过滤项目
|
||||||
// 1. 如果 show 为 false,则不显示
|
// 1. 如果 show 为 false,则不显示
|
||||||
// 2. 如果 admin 为 true 且不是管理员模式,则不显示
|
// 2. 如果 admin 为 true 且不是管理员模式,则不显示
|
||||||
|
// 3. 根据分类过滤
|
||||||
const filteredProjects = projects.filter(project => {
|
const filteredProjects = projects.filter(project => {
|
||||||
// 首先检查 show 字段,如果为 false 则直接不显示
|
// 首先检查 show 字段,如果为 false 则直接不显示
|
||||||
if (project.show === false) {
|
if (project.show === false) {
|
||||||
@@ -40,27 +68,85 @@ function ProjectsSection({ projects }) {
|
|||||||
return false; // 隐藏需要 admin 权限的项目
|
return false; // 隐藏需要 admin 权限的项目
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据分类过滤
|
||||||
|
if (category === 'develop' && project.develop !== true) {
|
||||||
|
return false; // 只显示自制项目
|
||||||
|
}
|
||||||
|
if (category === 'deploy' && project.develop !== false) {
|
||||||
|
return false; // 只显示自部署项目
|
||||||
|
}
|
||||||
|
|
||||||
return true; // 显示其他所有项目
|
return true; // 显示其他所有项目
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 计算总页数
|
||||||
|
const totalPages = Math.ceil(filteredProjects.length / itemsPerPage);
|
||||||
|
|
||||||
|
// 获取当前页的项目
|
||||||
|
const startIndex = currentPage * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const currentPageProjects = filteredProjects.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// 处理分页点击
|
||||||
|
const handlePageClick = (pageIndex) => {
|
||||||
|
setCurrentPage(pageIndex);
|
||||||
|
// 滚动到项目区域顶部
|
||||||
|
window.scrollTo({
|
||||||
|
top: document.querySelector('.projects-section').offsetTop - 20,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理分类切换
|
||||||
|
const handleCategoryChange = (newCategory) => {
|
||||||
|
setCategory(newCategory);
|
||||||
|
setCurrentPage(0); // 切换分类时重置到第一页
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="projects-section">
|
<section className="projects-section">
|
||||||
<h2 className="section-title">
|
<div className="section-header">
|
||||||
<span className="title-icon">🎯</span>
|
<h2 className="section-title">
|
||||||
精选项目
|
<span className="title-icon">🎯</span>
|
||||||
</h2>
|
全部项目
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* 分类按钮 */}
|
||||||
|
<div className="category-buttons">
|
||||||
|
<button
|
||||||
|
className={`category-btn ${category === 'all' ? 'active' : ''}`}
|
||||||
|
onClick={() => handleCategoryChange('all')}
|
||||||
|
>
|
||||||
|
全部项目
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`category-btn ${category === 'develop' ? 'active' : ''}`}
|
||||||
|
onClick={() => handleCategoryChange('develop')}
|
||||||
|
>
|
||||||
|
自制项目
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`category-btn ${category === 'deploy' ? 'active' : ''}`}
|
||||||
|
onClick={() => handleCategoryChange('deploy')}
|
||||||
|
>
|
||||||
|
自部署项目
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="projects-grid">
|
<div className="projects-grid">
|
||||||
{filteredProjects.map(project => (
|
{currentPageProjects.map((project, index) => {
|
||||||
<a
|
const globalIndex = startIndex + index;
|
||||||
key={project.id}
|
return (
|
||||||
href={project.link}
|
<a
|
||||||
target="_blank"
|
key={globalIndex}
|
||||||
rel="noopener noreferrer"
|
href={project.link}
|
||||||
className={`project-card ${hoveredId === project.id ? 'hovered' : ''}`}
|
target="_blank"
|
||||||
onMouseEnter={() => setHoveredId(project.id)}
|
rel="noopener noreferrer"
|
||||||
onMouseLeave={() => setHoveredId(null)}
|
className={`project-card ${hoveredIndex === globalIndex ? 'hovered' : ''}`}
|
||||||
>
|
onMouseEnter={() => setHoveredIndex(globalIndex)}
|
||||||
|
onMouseLeave={() => setHoveredIndex(null)}
|
||||||
|
>
|
||||||
{project.develop === true && (
|
{project.develop === true && (
|
||||||
<div className="develop-badge" title="独立开发">
|
<div className="develop-badge" title="独立开发">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
@@ -72,11 +158,9 @@ function ProjectsSection({ projects }) {
|
|||||||
<div className="project-header">
|
<div className="project-header">
|
||||||
<div className="project-icon">
|
<div className="project-icon">
|
||||||
<img
|
<img
|
||||||
src={project.icon || getFavicon(project.link)}
|
src={getProjectIconUrl(project.link)}
|
||||||
alt={project.title}
|
alt={project.title}
|
||||||
onError={(e) => {
|
onError={(e) => handleProjectIconError(e, project)}
|
||||||
e.target.src = 'https://api.iconify.design/mdi:web.svg';
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="project-title">{project.title}</h3>
|
<h3 className="project-title">{project.title}</h3>
|
||||||
@@ -92,9 +176,24 @@ function ProjectsSection({ projects }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 分页指示器 */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="pagination-dots">
|
||||||
|
{Array.from({ length: totalPages }, (_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className={`pagination-dot ${currentPage === index ? 'active' : ''}`}
|
||||||
|
onClick={() => handlePageClick(index)}
|
||||||
|
aria-label={`跳转到第 ${index + 1} 页`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,60 +46,95 @@
|
|||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 24px;
|
padding: 16px;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-items {
|
.tech-items {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-item {
|
.tech-item {
|
||||||
transition: all 0.3s ease;
|
display: inline-flex;
|
||||||
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 {
|
.tech-badge-link {
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-item img {
|
.tech-badge {
|
||||||
height: 32px;
|
display: inline-flex;
|
||||||
max-width: 100%;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0;
|
||||||
|
height: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
max-height: 36px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-badge:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 6px 0 8px;
|
||||||
|
height: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
max-height: 36px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-icon img {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
display: block;
|
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;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-item:hover {
|
.badge-icon-placeholder {
|
||||||
transform: translateY(-3px);
|
width: 36px;
|
||||||
background: rgba(255, 255, 255, 0.25);
|
height: 36px;
|
||||||
box-shadow: 0 4px 15px rgba(82, 183, 136, 0.2);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-item:hover img {
|
.badge-text {
|
||||||
filter: brightness(1.1) drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15));
|
padding: 0 8px 0 6px;
|
||||||
|
height: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
max-height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
@@ -110,11 +145,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tech-items {
|
.tech-items {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 10px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tech-item img {
|
.tech-badge {
|
||||||
|
height: 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-icon {
|
||||||
|
padding: 0 4px 0 5px;
|
||||||
|
height: 30px;
|
||||||
|
width: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-icon img {
|
||||||
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-icon-placeholder {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-text {
|
||||||
|
padding: 0 6px 0 4px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.tech-items {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,21 @@ import './TechStackSection.css';
|
|||||||
function TechStackSection({ techstack }) {
|
function TechStackSection({ techstack }) {
|
||||||
if (!techstack || !techstack.items) return null;
|
if (!techstack || !techstack.items) return null;
|
||||||
|
|
||||||
|
// 获取API基础URL,用于处理图标路径
|
||||||
|
// 使用环境变量配置的API地址,默认为 nav.api.shumengya.top
|
||||||
|
const apiBaseUrl = process.env.REACT_APP_API_URL ||
|
||||||
|
(process.env.NODE_ENV === 'development'
|
||||||
|
? 'http://localhost:5000/api'
|
||||||
|
: 'https://nav.api.shumengya.top/api');
|
||||||
|
|
||||||
|
// 获取图标URL
|
||||||
|
const getIconUrl = (item) => {
|
||||||
|
if (item.svg) {
|
||||||
|
return `${apiBaseUrl}/logo/${item.svg}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="techstack-section">
|
<section className="techstack-section">
|
||||||
<h2 className="section-title">
|
<h2 className="section-title">
|
||||||
@@ -13,31 +28,53 @@ function TechStackSection({ techstack }) {
|
|||||||
|
|
||||||
<div className="techstack-container">
|
<div className="techstack-container">
|
||||||
<div className="tech-items">
|
<div className="tech-items">
|
||||||
{techstack.items.map((item, idx) => (
|
{techstack.items
|
||||||
<div key={idx} className="tech-item">
|
.filter(item => item.show !== false)
|
||||||
{item.link ? (
|
.map((item, idx) => {
|
||||||
<a
|
const iconUrl = getIconUrl(item);
|
||||||
href={item.link}
|
const backgroundColor = item.color || '#555555';
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
const badgeContent = (
|
||||||
title={item.name}
|
<div
|
||||||
|
className="tech-badge"
|
||||||
|
style={{
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
color: backgroundColor === '#FFFFFF' ? '#000000' : 'white'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<div className="badge-icon">
|
||||||
src={item.icon}
|
{iconUrl ? (
|
||||||
alt={item.name}
|
<img
|
||||||
loading="lazy"
|
src={iconUrl}
|
||||||
/>
|
alt={item.name}
|
||||||
</a>
|
loading="lazy"
|
||||||
) : (
|
/>
|
||||||
<img
|
) : (
|
||||||
src={item.icon}
|
<span className="badge-icon-placeholder">?</span>
|
||||||
alt={item.name}
|
)}
|
||||||
title={item.name}
|
</div>
|
||||||
loading="lazy"
|
<span className="badge-text">{item.name}</span>
|
||||||
/>
|
</div>
|
||||||
)}
|
);
|
||||||
</div>
|
|
||||||
))}
|
return (
|
||||||
|
<div key={idx} className="tech-item">
|
||||||
|
{item.link ? (
|
||||||
|
<a
|
||||||
|
href={item.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title={item.name}
|
||||||
|
className="tech-badge-link"
|
||||||
|
>
|
||||||
|
{badgeContent}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
badgeContent
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -11,7 +11,13 @@ root.render(
|
|||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
window.addEventListener('load', () => {
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
navigator.serviceWorker
|
||||||
|
.register(`${process.env.PUBLIC_URL || ''}/service-worker.js`)
|
||||||
|
.then((reg) => console.log('PWA SW registered', reg.scope))
|
||||||
|
.catch((e) => console.log('PWA SW registration failed', e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
reportWebVitals();
|
reportWebVitals();
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,8 +1,4 @@
|
|||||||
@echo off
|
@echo off
|
||||||
cd /d "%~dp0mengyaprofile-backend"
|
cd /d "%~dp0mengyaprofile-backend"
|
||||||
set RUN_MODE=development
|
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
|
python app.py
|
||||||
|
|||||||