chore: sync local updates
10
AGENTS.md
@@ -3,7 +3,7 @@
|
||||
## 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/`.
|
||||
- `mengyaprofile-backend/`: Flask API (`app.py`, `db.py`) plus SQLite(默认 `data/site.db`,由 `schema.sql` 定义)与静态资源 `data/logo/`、`data/background/`。首次启动若库为空会写入最小占位行;内容通过管理后台或直连数据库维护。
|
||||
- Root `*.bat`: Windows helper scripts for starting/building locally.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
@@ -22,14 +22,14 @@ 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`.
|
||||
- Windows / 根目录脚本: `dev.bat`、`build.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.
|
||||
- 站点内容:维护 `data/site.db`(表 `profile`、`project` / `project_tag` / `project_tech_tag`、`contact`、`techstack_section` / `techstack_item`),或通过 `/admin` 与 `X-Admin-Token` 调用 `/api/admin/*`。
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
@@ -39,9 +39,9 @@ npm run build # production build to ./build
|
||||
## 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.
|
||||
- PRs: describe behavior changes, link issues, include screenshots for UI changes, and call out any `schema.sql` / API shape changes.
|
||||
|
||||
## 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`).
|
||||
- Useful env vars: backend `RUN_MODE`, `DATA_DIR`, `DATABASE_PATH`(可选,默认 `DATA_DIR/site.db`), `BACKGROUND_DIR`, `ADMIN_TOKEN`(管理写接口,默认 `shumengya520`,生产务必修改), `PORT`; frontend `REACT_APP_API_URL` (use `.env.local`)。主页头像连点 5 次(2 秒内累计重置)弹出 token 验证,通过后进入 `/admin` 管理页;写操作须请求头 `X-Admin-Token`。
|
||||
|
||||
98
README.md
@@ -40,8 +40,8 @@ docker-compose up -d --build
|
||||
- 📱 **响应式布局**: 完美适配桌面端和移动端
|
||||
- 🔄 **前后端分离**: React + Python Flask 架构
|
||||
- 🐳 **Docker 支持**: 一键部署,开箱即用
|
||||
- 💾 **数据持久化**: 配置文件外部存储
|
||||
- ⚡ **快速灵活**: 通过 JSON 配置文件轻松管理内容
|
||||
- 💾 **数据持久化**: SQLite(`data/site.db`)+ 管理后台
|
||||
- ⚡ **内容管理**: 通过 `/admin` 或 Admin API 维护站点数据
|
||||
- 🎯 **三大模块**: 个人信息、全部项目、联系方式
|
||||
- 🔐 **权限控制**: 管理员模式隐藏私密项目
|
||||
|
||||
@@ -52,10 +52,7 @@ docker-compose up -d --build
|
||||
├── mengyaprofile-backend/ # 后端 Python Flask 项目
|
||||
│ ├── app.py # Flask 应用主文件
|
||||
│ ├── requirements.txt # Python 依赖
|
||||
│ └── data/ # 数据配置文件
|
||||
│ ├── profile.json # 个人信息
|
||||
│ ├── projects.json # 项目列表
|
||||
│ └── contacts.json # 联系方式
|
||||
│ └── data/ # SQLite(site.db)与 logo 等静态资源
|
||||
│
|
||||
└── mengyaprofile-frontend/ # 前端 React 项目
|
||||
├── public/ # 静态资源
|
||||
@@ -111,93 +108,14 @@ npm start
|
||||
|
||||
在浏览器中打开 `http://localhost:3000` 即可查看你的个人主页!
|
||||
|
||||
## 功能模块
|
||||
## 功能模块与数据
|
||||
|
||||
### 1️⃣ 个人信息模块
|
||||
个人信息、项目列表、联系方式、技术栈等内容均存储在 **`mengyaprofile-backend/data/site.db`**(SQLite),表结构见 `mengyaprofile-backend/schema.sql`。
|
||||
|
||||
展示个人基本信息:
|
||||
- ✨ 昵称
|
||||
- 🖼️ 头像(支持动画效果)
|
||||
- 📝 个人介绍
|
||||
- 💼 技术定位(Full-Stack / Backend / DevOps)
|
||||
- 💡 个人座右铭
|
||||
- **浏览**: 首页读取 `/api/all` 等接口。
|
||||
- **编辑**: 打开 **`/admin`**(需管理员 token,见后端环境变量 `ADMIN_TOKEN`),在后台表单中维护;或直接调用 `/api/admin/*` 并携带请求头 `X-Admin-Token`。
|
||||
|
||||
**配置文件**: `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": "💬"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
空库首次启动时只会创建最小占位数据(空昵称、默认「技术栈」标题),其余请在管理后台中填写。
|
||||
|
||||
## 技术栈
|
||||
|
||||
|
||||
2
build-backend-docker.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
call "%~dp0mengyaprofile-backend\docker-build.bat"
|
||||
@@ -1,42 +0,0 @@
|
||||
@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
|
||||
3
build.bat
Normal file
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
cd /d "%~dp0mengyaprofile-frontend"
|
||||
call npm run build
|
||||
4
dev.bat
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
set "ROOT=%~dp0"
|
||||
start "backend" cmd /k cd /d "%ROOT%mengyaprofile-backend" ^&^& python app.py
|
||||
start "frontend" cmd /k cd /d "%ROOT%mengyaprofile-frontend" ^&^& npm start
|
||||
@@ -16,3 +16,8 @@ ENV/
|
||||
.gitignore
|
||||
README.md
|
||||
*.md
|
||||
|
||||
# 运行时数据不入镜像(挂载卷提供持久化)
|
||||
data/site.db
|
||||
data/*.db
|
||||
**/*.db
|
||||
|
||||
1
mengyaprofile-backend/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
docker-entrypoint.sh text eol=lf
|
||||
5
mengyaprofile-backend/.gitignore
vendored
@@ -40,3 +40,8 @@ Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# SQLite(站点数据;部署时随卷持久化)
|
||||
data/site.db
|
||||
data/site.db-shm
|
||||
data/site.db-wal
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
# 使用 Python 官方镜像
|
||||
# 萌芽主页 API — 生产镜像(SQLite + 静态 logo)
|
||||
FROM python:3.11-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV RUN_MODE=production
|
||||
ENV DATA_DIR=/app/data
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
RUN_MODE=production \
|
||||
DATA_DIR=/app/data \
|
||||
PORT=5000
|
||||
|
||||
# 复制依赖文件
|
||||
# 依赖
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --no-compile -r requirements.txt
|
||||
|
||||
# 安装依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
# 应用代码
|
||||
COPY app.py db.py schema.sql ./
|
||||
|
||||
# 复制应用代码
|
||||
COPY app.py .
|
||||
# 技术栈 SVG:镜像内 ./data/logo;/app/logo-dist 供 entrypoint 在「空数据卷」时复制到挂载目录
|
||||
COPY data/logo ./data/logo
|
||||
COPY data/logo /app/logo-dist
|
||||
COPY docker-entrypoint.sh /app/docker-entrypoint.sh
|
||||
RUN chmod +x /app/docker-entrypoint.sh && mkdir -p /app/data/background
|
||||
|
||||
# 创建数据目录(如果挂载了外部卷,这个目录会被覆盖)
|
||||
RUN mkdir -p /app/data/logo
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5000
|
||||
|
||||
# 启动应用
|
||||
CMD ["python", "app.py"]
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:5000/api')" || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
CMD ["sh", "-c", "exec gunicorn --bind 0.0.0.0:${PORT:-5000} --workers 2 --threads 2 --timeout 60 --access-logfile - --error-logfile - app:app"]
|
||||
|
||||
@@ -23,11 +23,9 @@ python app.py
|
||||
- `GET /api/contacts` - 获取联系方式
|
||||
- `GET /api/all` - 获取所有数据
|
||||
|
||||
## 数据配置
|
||||
## 数据存储
|
||||
|
||||
数据存储在 `data` 目录下的 JSON 文件中:
|
||||
- `profile.json` - 个人信息
|
||||
- `projects.json` - 项目列表
|
||||
- `contacts.json` - 联系方式
|
||||
站点内容在 SQLite 数据库中(默认路径 `data/site.db`,可用环境变量 `DATABASE_PATH` / `DATA_DIR` 覆盖),表结构见本目录下 `schema.sql`。
|
||||
|
||||
可根据需要编辑这些文件来更新网站内容。
|
||||
- 首次启动若数据库为空,会创建库并写入空的 profile 行与技术栈区块标题占位。
|
||||
- 通过前端管理页(`/admin`)或 `PUT/POST /api/admin/*`(请求头 `X-Admin-Token`)维护资料、项目、联系方式与技术栈。
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
from flask import Flask, jsonify, send_from_directory, request
|
||||
from flask_cors import CORS
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
|
||||
from db import (
|
||||
init_storage,
|
||||
get_profile,
|
||||
get_projects,
|
||||
get_contacts,
|
||||
get_techstack,
|
||||
get_all,
|
||||
update_profile,
|
||||
create_project,
|
||||
update_project,
|
||||
delete_project,
|
||||
create_contact,
|
||||
update_contact,
|
||||
delete_contact,
|
||||
set_techstack_title,
|
||||
create_techstack_item,
|
||||
update_techstack_item,
|
||||
delete_techstack_item,
|
||||
)
|
||||
|
||||
# 检测运行模式:通过环境变量控制
|
||||
RUN_MODE = os.environ.get('RUN_MODE', 'development') # development 或 production
|
||||
|
||||
@@ -19,6 +38,8 @@ HAS_FRONTEND_BUILD = os.path.exists(FRONTEND_BUILD_PATH) and os.path.isdir(FRONT
|
||||
# 支持通过环境变量配置,默认在数据目录中
|
||||
BACKGROUND_DIR = os.environ.get('BACKGROUND_DIR', os.path.join(DATA_DIR, 'background'))
|
||||
|
||||
ADMIN_TOKEN = os.environ.get('ADMIN_TOKEN', 'shumengya520')
|
||||
|
||||
if RUN_MODE == 'production' and HAS_FRONTEND_BUILD:
|
||||
# 生产环境:使用构建后的前端(如果存在)
|
||||
app = Flask(__name__, static_folder=FRONTEND_BUILD_PATH, static_url_path='')
|
||||
@@ -26,50 +47,39 @@ else:
|
||||
# 开发环境或纯后端模式:只提供 API
|
||||
app = Flask(__name__)
|
||||
|
||||
CORS(app) # 允许跨域请求
|
||||
CORS(app, allow_headers=['Content-Type', 'X-Admin-Token'])
|
||||
|
||||
init_storage()
|
||||
|
||||
|
||||
def require_admin():
|
||||
if request.headers.get('X-Admin-Token', '') != ADMIN_TOKEN:
|
||||
return jsonify({'error': 'unauthorized'}), 401
|
||||
return None
|
||||
|
||||
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():
|
||||
def get_profile_route():
|
||||
"""获取个人基本信息"""
|
||||
data = load_json_file('profile.json')
|
||||
data = get_profile()
|
||||
if data:
|
||||
return jsonify(data)
|
||||
return jsonify({"error": "Profile没有找到"}), 404
|
||||
|
||||
@app.route('/api/projects', methods=['GET'])
|
||||
def get_projects():
|
||||
def get_projects_route():
|
||||
"""获取全部项目列表"""
|
||||
data = load_json_file('projects.json')
|
||||
if data:
|
||||
return jsonify(data)
|
||||
return jsonify({"error": "Projects没有找到"}), 404
|
||||
return jsonify(get_projects())
|
||||
|
||||
@app.route('/api/contacts', methods=['GET'])
|
||||
def get_contacts():
|
||||
def get_contacts_route():
|
||||
"""获取联系方式"""
|
||||
data = load_json_file('contacts.json')
|
||||
if data:
|
||||
return jsonify(data)
|
||||
return jsonify({"error": "Contacts没有找到"}), 404
|
||||
return jsonify(get_contacts())
|
||||
|
||||
@app.route('/api/techstack', methods=['GET'])
|
||||
def get_techstack():
|
||||
def get_techstack_route():
|
||||
"""获取技术栈"""
|
||||
data = load_json_file('techstack.json')
|
||||
if data:
|
||||
return jsonify(data)
|
||||
return jsonify({"error": "Tech stack没有找到"}), 404
|
||||
return jsonify(get_techstack())
|
||||
|
||||
@app.route('/api/logo/<filename>', methods=['GET'])
|
||||
def get_logo(filename):
|
||||
@@ -144,19 +154,132 @@ def get_background_image(filename):
|
||||
return jsonify({"error": f"背景图片未找到: {filename}"}), 404
|
||||
|
||||
@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
|
||||
})
|
||||
def get_all_route():
|
||||
"""获取聚合站点数据;带管理员令牌时可列出隐藏的联系方式。"""
|
||||
admin_ok = request.headers.get('X-Admin-Token', '') == ADMIN_TOKEN
|
||||
return jsonify(get_all(include_hidden_contacts=admin_ok))
|
||||
|
||||
|
||||
@app.route('/api/admin/ping', methods=['GET'])
|
||||
def admin_ping():
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
return ('', 204)
|
||||
|
||||
|
||||
@app.route('/api/admin/profile', methods=['PUT'])
|
||||
def admin_put_profile():
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
body = request.get_json(silent=True) or {}
|
||||
update_profile(body)
|
||||
return jsonify(get_profile())
|
||||
|
||||
|
||||
@app.route('/api/admin/projects', methods=['POST'])
|
||||
def admin_post_project():
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not body.get('title'):
|
||||
return jsonify({'error': 'title required'}), 400
|
||||
pid = create_project(body)
|
||||
return jsonify({'id': pid}), 201
|
||||
|
||||
|
||||
@app.route('/api/admin/projects/<int:pid>', methods=['PUT'])
|
||||
def admin_put_project(pid):
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
body = request.get_json(silent=True) or {}
|
||||
update_project(pid, body)
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/admin/projects/<int:pid>', methods=['DELETE'])
|
||||
def admin_delete_project(pid):
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
delete_project(pid)
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/admin/contacts', methods=['POST'])
|
||||
def admin_post_contact():
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not body.get('type'):
|
||||
return jsonify({'error': 'type required'}), 400
|
||||
cid = create_contact(body)
|
||||
return jsonify({'id': cid}), 201
|
||||
|
||||
|
||||
@app.route('/api/admin/contacts/<int:cid>', methods=['PUT'])
|
||||
def admin_put_contact(cid):
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
body = request.get_json(silent=True) or {}
|
||||
update_contact(cid, body)
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/admin/contacts/<int:cid>', methods=['DELETE'])
|
||||
def admin_delete_contact(cid):
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
delete_contact(cid)
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/admin/techstack/title', methods=['PUT'])
|
||||
def admin_put_techstack_title():
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
body = request.get_json(silent=True) or {}
|
||||
set_techstack_title(body.get('title', ''))
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/admin/techstack/items', methods=['POST'])
|
||||
def admin_post_tech_item():
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
body = request.get_json(silent=True) or {}
|
||||
if not body.get('name'):
|
||||
return jsonify({'error': 'name required'}), 400
|
||||
iid = create_techstack_item(body)
|
||||
return jsonify({'id': iid}), 201
|
||||
|
||||
|
||||
@app.route('/api/admin/techstack/items/<int:iid>', methods=['PUT'])
|
||||
def admin_put_tech_item(iid):
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
body = request.get_json(silent=True) or {}
|
||||
update_techstack_item(iid, body)
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/admin/techstack/items/<int:iid>', methods=['DELETE'])
|
||||
def admin_delete_tech_item(iid):
|
||||
err = require_admin()
|
||||
if err:
|
||||
return err
|
||||
delete_techstack_item(iid)
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def index():
|
||||
@@ -176,6 +299,7 @@ def index():
|
||||
"mode": RUN_MODE,
|
||||
"note": "这是一个纯后端API服务,前端请访问独立的前端应用",
|
||||
"api_base": "https://nav.api.shumengya.top/api",
|
||||
"storage": "SQLite (site.db)",
|
||||
"endpoints": {
|
||||
"/api/profile": "获取个人信息",
|
||||
"/api/techstack": "获取技术栈",
|
||||
@@ -208,6 +332,7 @@ def api_info():
|
||||
"message": "萌芽主页 后端API",
|
||||
"author":"树萌芽",
|
||||
"version": "1.0.0",
|
||||
"storage": "SQLite (site.db)",
|
||||
"endpoints": {
|
||||
"/api/profile": "获取个人信息",
|
||||
"/api/techstack": "获取技术栈",
|
||||
|
||||
|
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 |
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"contacts": [
|
||||
{
|
||||
"type": "email",
|
||||
"label": "个人邮箱",
|
||||
"value": "mail@smyhub.com",
|
||||
"link": "mailto:mail@smyhub.com",
|
||||
"icon": "https://img.shumengya.top/i/2026/02/10/698b21a5e2c26.png"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
mengyaprofile-backend/data/logo/alibabacloud.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AlibabaCloud</title><path d="M14.752 4.64h5.274C22.242 4.64 24 6.475 24 8.691V15.8a3.947 3.947 0 01-3.974 3.975h-5.274l1.299-1.835 3.822-1.222c.688-.23 1.146-.918 1.146-1.605v-5.81c0-.687-.458-1.375-1.146-1.605L16.05 6.475l-1.3-1.835zM2.98 15.111c0 .688.46 1.376 1.147 1.606l3.822 1.146 1.3 1.835H3.974A3.947 3.947 0 010 15.723V8.69c0-2.216 1.758-4.05 3.975-4.05h5.273L7.95 6.474 4.127 7.697c-.688.23-1.146.918-1.146 1.606v5.808z" fill="#FF6A00"></path><path d="M16.051 11.213H8.025v1.835h8.026v-1.835z" fill="#FF6A00"></path></svg>
|
||||
|
After Width: | Height: | Size: 656 B |
1
mengyaprofile-backend/data/logo/antigravity.svg
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
1
mengyaprofile-backend/data/logo/apple.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M11.932 6.908c.95 0 2.727-1.291 4.595-1.1.782.032 2.976.316 4.388 2.38-.113.069-2.622 1.528-2.593 4.565.034 3.617 3.166 4.828 3.221 4.85-.029.086-.506 1.723-1.658 3.416-1.002 1.463-2.039 2.919-3.675 2.95-1.606.03-2.125-.955-3.96-.955s-2.409.923-3.931.984c-1.581.06-2.78-1.58-3.79-3.037-2.065-2.98-3.64-8.422-1.527-12.087 1.051-1.824 2.93-2.98 4.969-3.009 1.549-.032 3.011 1.043 3.96 1.043zM16.552 0c.153 1.407-.411 2.817-1.251 3.833-.837 1.013-2.214 1.804-3.555 1.7-.185-1.378.495-2.814 1.27-3.712C13.883.805 15.346.05 16.553 0z"></path></svg>
|
||||
|
After Width: | Height: | Size: 729 B |
1
mengyaprofile-backend/data/logo/chatgpt.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 509.639"><path fill="#fff" d="M115.612 0h280.775C459.974 0 512 52.026 512 115.612v278.415c0 63.587-52.026 115.613-115.613 115.613H115.612C52.026 509.64 0 457.614 0 394.027V115.612C0 52.026 52.026 0 115.612 0z"/><path fill-rule="nonzero" d="M412.037 221.764a90.834 90.834 0 004.648-28.67 90.79 90.79 0 00-12.443-45.87c-16.37-28.496-46.738-46.089-79.605-46.089-6.466 0-12.943.683-19.264 2.04a90.765 90.765 0 00-67.881-30.515h-.576c-.059.002-.149.002-.216.002-39.807 0-75.108 25.686-87.346 63.554-25.626 5.239-47.748 21.31-60.682 44.03a91.873 91.873 0 00-12.407 46.077 91.833 91.833 0 0023.694 61.553 90.802 90.802 0 00-4.649 28.67 90.804 90.804 0 0012.442 45.87c16.369 28.504 46.74 46.087 79.61 46.087a91.81 91.81 0 0019.253-2.04 90.783 90.783 0 0067.887 30.516h.576l.234-.001c39.829 0 75.119-25.686 87.357-63.588 25.626-5.242 47.748-21.312 60.682-44.033a91.718 91.718 0 0012.383-46.035 91.83 91.83 0 00-23.693-61.553l-.004-.005zM275.102 413.161h-.094a68.146 68.146 0 01-43.611-15.8 56.936 56.936 0 002.155-1.221l72.54-41.901a11.799 11.799 0 005.962-10.251V241.651l30.661 17.704c.326.163.55.479.596.84v84.693c-.042 37.653-30.554 68.198-68.21 68.273h.001zm-146.689-62.649a68.128 68.128 0 01-9.152-34.085c0-3.904.341-7.817 1.005-11.663.539.323 1.48.897 2.155 1.285l72.54 41.901a11.832 11.832 0 0011.918-.002l88.563-51.137v35.408a1.1 1.1 0 01-.438.94l-73.33 42.339a68.43 68.43 0 01-34.11 9.12 68.359 68.359 0 01-59.15-34.11l-.001.004zm-19.083-158.36a68.044 68.044 0 0135.538-29.934c0 .625-.036 1.731-.036 2.5v83.801l-.001.07a11.79 11.79 0 005.954 10.242l88.564 51.13-30.661 17.704a1.096 1.096 0 01-1.034.093l-73.337-42.375a68.36 68.36 0 01-34.095-59.143 68.412 68.412 0 019.112-34.085l-.004-.003zm251.907 58.621l-88.563-51.137 30.661-17.697a1.097 1.097 0 011.034-.094l73.337 42.339c21.109 12.195 34.132 34.746 34.132 59.132 0 28.604-17.849 54.199-44.686 64.078v-86.308c.004-.032.004-.065.004-.096 0-4.219-2.261-8.119-5.919-10.217zm30.518-45.93c-.539-.331-1.48-.898-2.155-1.286l-72.54-41.901a11.842 11.842 0 00-5.958-1.611c-2.092 0-4.15.558-5.957 1.611l-88.564 51.137v-35.408l-.001-.061a1.1 1.1 0 01.44-.88l73.33-42.303a68.301 68.301 0 0134.108-9.129c37.704 0 68.281 30.577 68.281 68.281a68.69 68.69 0 01-.984 11.545v.005zm-191.843 63.109l-30.668-17.704a1.09 1.09 0 01-.596-.84v-84.692c.016-37.685 30.593-68.236 68.281-68.236a68.332 68.332 0 0143.689 15.804 63.09 63.09 0 00-2.155 1.222l-72.54 41.9a11.794 11.794 0 00-5.961 10.248v.068l-.05 102.23zm16.655-35.91l39.445-22.782 39.444 22.767v45.55l-39.444 22.767-39.445-22.767v-45.535z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1
mengyaprofile-backend/data/logo/claude-ai.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 509.64"><path fill="#D77655" d="M115.612 0h280.775C459.974 0 512 52.026 512 115.612v278.415c0 63.587-52.026 115.612-115.613 115.612H115.612C52.026 509.639 0 457.614 0 394.027V115.612C0 52.026 52.026 0 115.612 0z"/><path fill="#FCF2EE" fill-rule="nonzero" d="M142.27 316.619l73.655-41.326 1.238-3.589-1.238-1.996-3.589-.001-12.31-.759-42.084-1.138-36.498-1.516-35.361-1.896-8.897-1.895-8.34-10.995.859-5.484 7.482-5.03 10.717.935 23.683 1.617 35.537 2.452 25.782 1.517 38.193 3.968h6.064l.86-2.451-2.073-1.517-1.618-1.517-36.776-24.922-39.81-26.338-20.852-15.166-11.273-7.683-5.687-7.204-2.451-15.721 10.237-11.273 13.75.935 3.513.936 13.928 10.716 29.749 23.027 38.848 28.612 5.687 4.727 2.275-1.617.278-1.138-2.553-4.271-21.13-38.193-22.546-38.848-10.035-16.101-2.654-9.655c-.935-3.968-1.617-7.304-1.617-11.374l11.652-15.823 6.445-2.073 15.545 2.073 6.547 5.687 9.655 22.092 15.646 34.78 24.265 47.291 7.103 14.028 3.791 12.992 1.416 3.968 2.449-.001v-2.275l1.997-26.641 3.69-32.707 3.589-42.084 1.239-11.854 5.863-14.206 11.652-7.683 9.099 4.348 7.482 10.716-1.036 6.926-4.449 28.915-8.72 45.294-5.687 30.331h3.313l3.792-3.791 15.342-20.372 25.782-32.227 11.374-12.789 13.27-14.129 8.517-6.724 16.1-.001 11.854 17.617-5.307 18.199-16.581 21.029-13.75 17.819-19.716 26.54-12.309 21.231 1.138 1.694 2.932-.278 44.536-9.479 24.062-4.347 28.714-4.928 12.992 6.066 1.416 6.167-5.106 12.613-30.71 7.583-36.018 7.204-53.636 12.689-.657.48.758.935 24.164 2.275 10.337.556h25.301l47.114 3.514 12.309 8.139 7.381 9.959-1.238 7.583-18.957 9.655-25.579-6.066-59.702-14.205-20.474-5.106-2.83-.001v1.694l17.061 16.682 31.266 28.233 39.152 36.397 1.997 8.999-5.03 7.102-5.307-.758-34.401-25.883-13.27-11.651-30.053-25.302-1.996-.001v2.654l6.926 10.136 36.574 54.975 1.895 16.859-2.653 5.485-9.479 3.311-10.414-1.895-21.408-30.054-22.092-33.844-17.819-30.331-2.173 1.238-10.515 113.261-4.929 5.788-11.374 4.348-9.478-7.204-5.03-11.652 5.03-23.027 6.066-30.052 4.928-23.886 4.449-29.674 2.654-9.858-.177-.657-2.173.278-22.37 30.71-34.021 45.977-26.919 28.815-6.445 2.553-11.173-5.789 1.037-10.337 6.243-9.2 37.257-47.392 22.47-29.371 14.508-16.961-.101-2.451h-.859l-98.954 64.251-17.618 2.275-7.583-7.103.936-11.652 3.589-3.791 29.749-20.474-.101.102.024.101z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
mengyaprofile-backend/data/logo/claude.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fill-rule="nonzero"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
mengyaprofile-backend/data/logo/claudecode.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Antigravity</title><path clip-rule="evenodd" d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z" fill="#D97757" fill-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 424 B |
1
mengyaprofile-backend/data/logo/cloudflare.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cloudflare</title><path d="M16.493 17.4c.135-.52.08-.983-.161-1.338-.215-.328-.592-.519-1.05-.519l-8.663-.109a.148.148 0 01-.135-.082c-.027-.054-.027-.109-.027-.163.027-.082.108-.164.189-.164l8.744-.11c1.05-.054 2.153-.9 2.556-1.937l.511-1.31c.027-.055.027-.11.027-.164C17.92 8.91 15.66 7 12.942 7c-2.503 0-4.628 1.638-5.381 3.903a2.432 2.432 0 00-1.803-.491c-1.21.109-2.153 1.092-2.287 2.32-.027.328 0 .628.054.9C1.56 13.688 0 15.326 0 17.319c0 .19.027.355.027.545 0 .082.08.137.161.137h15.983c.08 0 .188-.055.215-.164l.107-.437" fill="#F38020"></path><path d="M19.238 11.75h-.242c-.054 0-.108.054-.135.109l-.35 1.2c-.134.52-.08.983.162 1.338.215.328.592.518 1.05.518l1.855.11c.054 0 .108.027.135.082.027.054.027.109.027.163-.027.082-.108.164-.188.164l-1.91.11c-1.05.054-2.153.9-2.557 1.937l-.134.355c-.027.055.026.137.107.137h6.592c.081 0 .162-.055.162-.137.107-.41.188-.846.188-1.31-.027-2.62-2.153-4.777-4.762-4.777" fill="#FCAD32"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
mengyaprofile-backend/data/logo/codex.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path d="M19.503 0H4.496A4.496 4.496 0 000 4.496v15.007A4.496 4.496 0 004.496 24h15.007A4.496 4.496 0 0024 19.503V4.496A4.496 4.496 0 0019.503 0z" fill="#fff"></path><path d="M9.064 3.344a4.578 4.578 0 012.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 00.043 0 4.55 4.55 0 013.046.275l.047.022.116.057a4.581 4.581 0 012.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 01-.134 1.223.123.123 0 00.03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 01-2.201 1.388.123.123 0 00-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 00-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 01-1.945-.466 4.544 4.544 0 01-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 01-.37-.961 4.582 4.582 0 01-.014-2.298.124.124 0 00.006-.056.085.085 0 00-.027-.048 4.467 4.467 0 01-1.034-1.651 3.896 3.896 0 01-.251-1.192 5.189 5.189 0 01.141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 00.065-.066 4.51 4.51 0 01.829-1.615 4.535 4.535 0 011.837-1.388zm3.482 10.565a.637.637 0 000 1.272h3.636a.637.637 0 100-1.272h-3.636zM8.462 9.23a.637.637 0 00-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 101.095.649l1.454-2.455a.636.636 0 00.005-.64L8.462 9.23z" fill="url(#lobe-icons-codex-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-codex-_R_0_" x1="12" x2="12" y1="3" y2="21"><stop stop-color="#B1A7FF"></stop><stop offset=".5" stop-color="#7A9DFF"></stop><stop offset="1" stop-color="#3941FF"></stop></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
mengyaprofile-backend/data/logo/cursor.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cursor</title><path d="M22.106 5.68L12.5.135a.998.998 0 00-.998 0L1.893 5.68a.84.84 0 00-.419.726v11.186c0 .3.16.577.42.727l9.607 5.547a.999.999 0 00.998 0l9.608-5.547a.84.84 0 00.42-.727V6.407a.84.84 0 00-.42-.726zm-.603 1.176L12.228 22.92c-.063.108-.228.064-.228-.061V12.34a.59.59 0 00-.295-.51l-9.11-5.26c-.107-.062-.063-.228.062-.228h18.55c.264 0 .428.286.296.514z"></path></svg>
|
||||
|
After Width: | Height: | Size: 547 B |
3
mengyaprofile-backend/data/logo/fastapi.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
|
||||
<path fill="#009688" d="M12 0C5.375 0 0 5.375 0 12c0 6.627 5.375 12 12 12 6.626 0 12-5.373 12-12 0-6.625-5.373-12-12-12zm-.624 21.62v-7.528H7.19L13.203 2.38v7.528h4.029L11.376 21.62z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
2
mengyaprofile-backend/data/logo/flutter.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_flutter</title><polyline points="15.383 18.316 18.744 15.042 27.093 15.042 19.697 22.438 15.383 18.316 15.383 18.316 15.383 18.316 15.383 18.316 15.383 18.316" style="fill:#40d0fd"/><polygon points="4.907 16.125 9.106 20.424 27.093 2.287 18.744 2.287 4.907 16.125" style="fill:#41d0fd;isolation:isolate"/><polygon points="11.176 22.479 15.435 26.675 19.697 22.438 15.383 18.316 11.176 22.479" style="fill:#1fbcfd"/><polygon points="15.435 26.675 19.697 22.438 26.989 29.813 18.593 29.813 15.435 26.675" style="fill:#095a9d"/><polygon points="15.435 26.675 19.406 25.354 18.068 24.057 15.435 26.675" style="fill:#0e5199"/></svg>
|
||||
|
After Width: | Height: | Size: 852 B |
1
mengyaprofile-backend/data/logo/gemini.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="#3186FF"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-0-_R_0_)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-1-_R_0_)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-2-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-0-_R_0_" x1="7" x2="11" y1="15.5" y2="12"><stop stop-color="#08B962"></stop><stop offset="1" stop-color="#08B962" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-1-_R_0_" x1="8" x2="11.5" y1="5.5" y2="11"><stop stop-color="#F94543"></stop><stop offset="1" stop-color="#F94543" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-2-_R_0_" x1="3.5" x2="17.5" y1="13.5" y2="12"><stop stop-color="#FABC12"></stop><stop offset=".46" stop-color="#FABC12" stop-opacity="0"></stop></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
mengyaprofile-backend/data/logo/geminicli.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini CLI</title><path d="M0 4.391A4.391 4.391 0 014.391 0h15.217A4.391 4.391 0 0124 4.391v15.217A4.391 4.391 0 0119.608 24H4.391A4.391 4.391 0 010 19.608V4.391z" fill="url(#lobe-icons-gemini-cli-_R_0_)"></path><path clip-rule="evenodd" d="M19.74 1.444a2.816 2.816 0 012.816 2.816v15.48a2.816 2.816 0 01-2.816 2.816H4.26a2.816 2.816 0 01-2.816-2.816V4.26A2.816 2.816 0 014.26 1.444h15.48zM7.236 8.564l7.752 3.728-7.752 3.727v2.802l9.557-4.596v-3.866L7.236 5.763v2.801z" fill="#1E1E2E" fill-rule="evenodd"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-cli-_R_0_" x1="24" x2="0" y1="6.587" y2="16.494"><stop stop-color="#EE4D5D"></stop><stop offset=".328" stop-color="#B381DD"></stop><stop offset=".476" stop-color="#207CFE"></stop></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 921 B |
1
mengyaprofile-backend/data/logo/github.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Github</title><path d="M12 0c6.63 0 12 5.276 12 11.79-.001 5.067-3.29 9.567-8.175 11.187-.6.118-.825-.25-.825-.56 0-.398.015-1.665.015-3.242 0-1.105-.375-1.813-.81-2.181 2.67-.295 5.475-1.297 5.475-5.822 0-1.297-.465-2.344-1.23-3.169.12-.295.54-1.503-.12-3.125 0 0-1.005-.324-3.3 1.209a11.32 11.32 0 00-3-.398c-1.02 0-2.04.133-3 .398-2.295-1.518-3.3-1.209-3.3-1.209-.66 1.622-.24 2.83-.12 3.125-.765.825-1.23 1.887-1.23 3.169 0 4.51 2.79 5.527 5.46 5.822-.345.294-.66.81-.765 1.577-.69.31-2.415.81-3.495-.973-.225-.354-.9-1.223-1.845-1.209-1.005.015-.405.56.015.781.51.28 1.095 1.327 1.23 1.666.24.663 1.02 1.93 4.035 1.385 0 .988.015 1.916.015 2.196 0 .31-.225.664-.825.56C3.303 21.374-.003 16.867 0 11.791 0 5.276 5.37 0 12 0z"></path></svg>
|
||||
|
After Width: | Height: | Size: 907 B |
1
mengyaprofile-backend/data/logo/githubcopilot.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>GithubCopilot</title><path d="M19.245 5.364c1.322 1.36 1.877 3.216 2.11 5.817.622 0 1.2.135 1.592.654l.73.964c.21.278.323.61.323.955v2.62c0 .339-.173.669-.453.868C20.239 19.602 16.157 21.5 12 21.5c-4.6 0-9.205-2.583-11.547-4.258-.28-.2-.452-.53-.453-.868v-2.62c0-.345.113-.679.321-.956l.73-.963c.392-.517.974-.654 1.593-.654l.029-.297c.25-2.446.81-4.213 2.082-5.52 2.461-2.54 5.71-2.851 7.146-2.864h.198c1.436.013 4.685.323 7.146 2.864zm-7.244 4.328c-.284 0-.613.016-.962.05-.123.447-.305.85-.57 1.108-1.05 1.023-2.316 1.18-2.994 1.18-.638 0-1.306-.13-1.851-.464-.516.165-1.012.403-1.044.996a65.882 65.882 0 00-.063 2.884l-.002.48c-.002.563-.005 1.126-.013 1.69.002.326.204.63.51.765 2.482 1.102 4.83 1.657 6.99 1.657 2.156 0 4.504-.555 6.985-1.657a.854.854 0 00.51-.766c.03-1.682.006-3.372-.076-5.053-.031-.596-.528-.83-1.046-.996-.546.333-1.212.464-1.85.464-.677 0-1.942-.157-2.993-1.18-.266-.258-.447-.661-.57-1.108-.32-.032-.64-.049-.96-.05zm-2.525 4.013c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zm5 0c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zM7.635 5.087c-1.05.102-1.935.438-2.385.906-.975 1.037-.765 3.668-.21 4.224.405.394 1.17.657 1.995.657h.09c.649-.013 1.785-.176 2.73-1.11.435-.41.705-1.433.675-2.47-.03-.834-.27-1.52-.63-1.813-.39-.336-1.275-.482-2.265-.394zm6.465.394c-.36.292-.6.98-.63 1.813-.03 1.037.24 2.06.675 2.47.968.957 2.136 1.104 2.776 1.11h.044c.825 0 1.59-.263 1.995-.657.555-.556.765-3.187-.21-4.224-.45-.468-1.335-.804-2.385-.906-.99-.088-1.875.058-2.265.394zM12 7.615c-.24 0-.525.015-.84.044.03.16.045.336.06.526l-.001.159a2.94 2.94 0 01-.014.25c.225-.022.425-.027.612-.028h.366c.187 0 .387.006.612.028-.015-.146-.015-.277-.015-.409.015-.19.03-.365.06-.526a9.29 9.29 0 00-.84-.044z"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
2
mengyaprofile-backend/data/logo/golang2.svg
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
1
mengyaprofile-backend/data/logo/google.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M23 12.245c0-.905-.075-1.565-.236-2.25h-10.54v4.083h6.186c-.124 1.014-.797 2.542-2.294 3.569l-.021.136 3.332 2.53.23.022C21.779 18.417 23 15.593 23 12.245z" fill="#4285F4"></path><path d="M12.225 23c3.03 0 5.574-.978 7.433-2.665l-3.542-2.688c-.948.648-2.22 1.1-3.891 1.1a6.745 6.745 0 01-6.386-4.572l-.132.011-3.465 2.628-.045.124C4.043 20.531 7.835 23 12.225 23z" fill="#34A853"></path><path d="M5.84 14.175A6.65 6.65 0 015.463 12c0-.758.138-1.491.361-2.175l-.006-.147-3.508-2.67-.115.054A10.831 10.831 0 001 12c0 1.772.436 3.447 1.197 4.938l3.642-2.763z" fill="#FBBC05"></path><path d="M12.225 5.253c2.108 0 3.529.892 4.34 1.638l3.167-3.031C17.787 2.088 15.255 1 12.225 1 7.834 1 4.043 3.469 2.197 7.062l3.63 2.763a6.77 6.77 0 016.398-4.572z" fill="#EB4335"></path></svg>
|
||||
|
After Width: | Height: | Size: 920 B |
1
mengyaprofile-backend/data/logo/grok.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path></svg>
|
||||
|
After Width: | Height: | Size: 756 B |
1
mengyaprofile-backend/data/logo/grok2.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 509.641"><path d="M115.612 0h280.776C459.975 0 512 52.026 512 115.612v278.416c0 63.587-52.025 115.613-115.612 115.613H115.612C52.026 509.641 0 457.615 0 394.028V115.612C0 52.026 52.026 0 115.612 0z"/><path fill="#fff" d="M213.235 306.019l178.976-180.002v.169l51.695-51.763c-.924 1.32-1.86 2.605-2.785 3.89-39.281 54.164-58.46 80.649-43.07 146.922l-.09-.101c10.61 45.11-.744 95.137-37.398 131.836-46.216 46.306-120.167 56.611-181.063 14.928l42.462-19.675c38.863 15.278 81.392 8.57 111.947-22.03 30.566-30.6 37.432-75.159 22.065-112.252-2.92-7.025-11.67-8.795-17.792-4.263l-124.947 92.341zm-25.786 22.437l-.033.034L68.094 435.217c7.565-10.429 16.957-20.294 26.327-30.149 26.428-27.803 52.653-55.359 36.654-94.302-21.422-52.112-8.952-113.177 30.724-152.898 41.243-41.254 101.98-51.661 152.706-30.758 11.23 4.172 21.016 10.114 28.638 15.639l-42.359 19.584c-39.44-16.563-84.629-5.299-112.207 22.313-37.298 37.308-44.84 102.003-1.128 143.81z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
mengyaprofile-backend/data/logo/huawei.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Huawei</title><path d="M10.341 17.042s.062-.061 0-.061C7.516 10.902 3.646 6.22 3.646 6.22S1.557 8.168 1.68 10.174c.061 1.52 1.228 2.37 1.228 2.37 1.843 1.763 6.266 4.012 7.31 4.499h.123zm-.737 1.52c0-.061-.123-.061-.123-.061l-7.371.243c.798 1.398 2.15 2.492 3.563 2.188.983-.243 3.194-1.763 3.87-2.25.123-.12.061-.12.061-.12zm.123-.67c.062-.06 0-.12 0-.12C6.471 15.581.206 12.3.206 12.3c-.553 1.763.184 3.161.184 3.161.798 1.702 2.334 2.189 2.334 2.189.676.303 1.413.303 1.413.303h5.529c.061 0 .061-.06.061-.06zm.492-14.831c-.308 0-1.168.243-1.168.243-1.965.486-2.395 2.249-2.395 2.249-.369 1.094 0 2.31 0 2.31.675 2.857 3.87 7.598 4.545 8.57l.062.062c.061 0 .061-.061.061-.061C12.43 5.796 10.22 3.06 10.22 3.06zm2.457 13.373c.061 0 .123-.061.123-.061.737-1.033 3.87-5.714 4.545-8.57 0 0 .369-1.399 0-2.31 0 0-.491-1.764-2.457-2.25 0 0-.553-.121-1.167-.243 0 0-2.211 2.796-1.106 13.312 0 .122.062.122.062.122zm1.72 2.067s-.062 0-.062.06v.122c.738.486 2.826 2.006 3.87 2.249 0 0 1.905.669 3.563-2.188l-7.371-.243zm9.398-6.261s-6.265 3.343-9.521 5.531c0 0-.062.06-.062.122 0 0 0 .06.062.06h5.651s.553 0 1.29-.303c0 0 1.536-.487 2.396-2.25 0-.06.737-1.458.184-3.16zM13.66 17.042s.061.06.122 0c1.045-.547 5.468-2.736 7.31-4.499 0 0 1.168-.911 1.23-2.37.122-2.067-1.967-3.951-1.967-3.951s-3.87 4.559-6.695 10.698c0 0-.062.06 0 .122z" fill="#C7000B"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
12
mengyaprofile-backend/data/logo/javascript.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M0,0 L256,0 L256,256 L0,256 L0,0 Z" fill="#F7DF1E">
|
||||
|
||||
</path>
|
||||
<path d="M67.311746,213.932292 L86.902654,202.076241 C90.6821079,208.777346 94.1202286,214.447137 102.367086,214.447137 C110.272203,214.447137 115.256076,211.354819 115.256076,199.326883 L115.256076,117.528787 L139.313575,117.528787 L139.313575,199.666997 C139.313575,224.58433 124.707759,235.925943 103.3984,235.925943 C84.1532952,235.925943 72.9819429,225.958603 67.3113397,213.93026" fill="#000000">
|
||||
|
||||
</path>
|
||||
<path d="M152.380952,211.354413 L171.969422,200.0128 C177.125994,208.433981 183.827911,214.619835 195.684368,214.619835 C205.652521,214.619835 212.009041,209.635962 212.009041,202.762159 C212.009041,194.513676 205.479416,191.592025 194.481168,186.78207 L188.468419,184.202565 C171.111213,176.81473 159.597308,167.53534 159.597308,147.944838 C159.597308,129.901308 173.344508,116.153295 194.825752,116.153295 C210.119924,116.153295 221.117765,121.48094 229.021663,135.400432 L210.29059,147.428775 C206.166146,140.040127 201.699556,137.119289 194.826159,137.119289 C187.78047,137.119289 183.312254,141.587098 183.312254,147.428775 C183.312254,154.646349 187.78047,157.568406 198.089956,162.036622 L204.103924,164.614095 C224.553448,173.378641 236.067352,182.313448 236.067352,202.418387 C236.067352,224.071924 219.055137,235.927975 196.200432,235.927975 C173.860978,235.927975 159.425829,225.274311 152.381359,211.354413" fill="#000000">
|
||||
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
24
mengyaprofile-backend/data/logo/json.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<defs>
|
||||
<linearGradient x1="15.7913036%" y1="14.7370493%" x2="91.0087401%" y2="85.2281246%" id="linearGradient-1">
|
||||
<stop stop-color="#000000" offset="0%">
|
||||
|
||||
</stop>
|
||||
<stop stop-color="#FFFFFF" offset="100%">
|
||||
|
||||
</stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="82.1356665%" y1="85.2373953%" x2="-3.11315442%" y2="14.7900497%" id="linearGradient-2">
|
||||
<stop stop-color="#000000" offset="0%">
|
||||
|
||||
</stop>
|
||||
<stop stop-color="#FFFFFF" offset="100%">
|
||||
|
||||
</stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M127.78336,190.559312 C184.419648,267.767856 239.846672,169.00968 239.765248,109.620512 C239.668928,39.402832 168.499792,0.158608 127.73696,0.158608 C62.309088,0.158608 0,54.232336 0,128.216032 C0,210.449632 71.424608,255.999984 127.73696,255.999984 C114.993808,254.165184 72.52704,245.065664 71.956288,147.253152 C71.572496,81.098592 93.5364,54.668336 127.64472,66.294672 C128.408608,66.577984 165.266992,81.117888 165.266992,128.61552 C165.266992,175.911392 127.78336,190.559312 127.78336,190.559312 L127.78336,190.559312 Z" fill="url(#linearGradient-1)">
|
||||
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
mengyaprofile-backend/data/logo/lobehub.svg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
4
mengyaprofile-backend/data/logo/markdown.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 8C0 5.79086 1.79086 4 4 4H20C22.2091 4 24 5.79086 24 8V16C24 18.2091 22.2091 20 20 20H4C1.79086 20 0 18.2091 0 16V8ZM4 6C2.89543 6 2 6.89543 2 8V16C2 17.1046 2.89543 18 4 18H20C21.1046 18 22 17.1046 22 16V8C22 6.89543 21.1046 6 20 6H4ZM5.68377 8.05132C6.09211 7.9152 6.54174 8.05566 6.8 8.4L9 11.3333L11.2 8.4C11.4583 8.05566 11.9079 7.9152 12.3162 8.05132C12.7246 8.18743 13 8.56957 13 9V15C13 15.5523 12.5523 16 12 16C11.4477 16 11 15.5523 11 15V12L9.8 13.6C9.61115 13.8518 9.31476 14 9 14C8.68524 14 8.38885 13.8518 8.2 13.6L7 12V15C7 15.5523 6.55228 16 6 16C5.44772 16 5 15.5523 5 15V9C5 8.56957 5.27543 8.18743 5.68377 8.05132ZM18 9C18 8.44772 17.5523 8 17 8C16.4477 8 16 8.44772 16 9V12.5858L15.7071 12.2929C15.3166 11.9024 14.6834 11.9024 14.2929 12.2929C13.9024 12.6834 13.9024 13.3166 14.2929 13.7071L16.2929 15.7071C16.6834 16.0976 17.3166 16.0976 17.7071 15.7071L19.7071 13.7071C20.0976 13.3166 20.0976 12.6834 19.7071 12.2929C19.3166 11.9024 18.6834 11.9024 18.2929 12.2929L18 12.5858V9Z" fill="#000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
mengyaprofile-backend/data/logo/mcp.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ModelContextProtocol</title><path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path><path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z"></path></svg>
|
||||
|
After Width: | Height: | Size: 978 B |
1
mengyaprofile-backend/data/logo/newapi.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>New API</title><path d="M23.078 16.34c-.506 1.323-1.198 2.519-2.117 3.562-2.378 2.696-5.374 4.057-8.971 4.098a.037.037 0 01-.024-.01.041.041 0 01-.013-.023.041.041 0 01.003-.025.037.037 0 01.019-.019c1.886-.779 3.454-1.973 4.625-3.639a10.148 10.148 0 001.626-3.677c.217-.98.33-1.955.282-2.942-.048-1.018-.152-1.601-.484-2.565-.386-1.12-.915-2.16-1.627-3.089-.883-1.154-1.876-1.87-2.9-2.779-.995-.88-2.19-2.623-1.059-3.754.384-.384.997-.59 1.838-.621 2.478-.09 5.011 1.636 6.597 3.453.75.86 1.38 1.798 1.865 2.837.486 1.041.814 2.122.978 3.246.133.915.117 1.441.092 2.365a10.82 10.82 0 01-.73 3.582z" fill="url(#lobe-icons-new-api-0-_R_0_)"></path><path d="M11.86.01a.041.041 0 01.009.049.038.038 0 01-.018.018C9.964.856 8.396 2.05 7.225 3.716a10.148 10.148 0 00-1.626 3.678c-.217.979-.33 1.955-.283 2.941.049 1.018.154 1.601.486 2.565.385 1.12.914 2.16 1.626 3.088.883 1.154 1.876 1.872 2.9 2.78.995.88 2.19 2.622 1.059 3.753-.385.385-.997.591-1.838.622-2.478.089-5.011-1.636-6.597-3.454-.75-.86-1.38-1.797-1.865-2.837a11.591 11.591 0 01-.978-3.246c-.133-.914-.117-1.44-.091-2.364.034-1.225.284-2.416.73-3.582.504-1.323 1.197-2.52 2.116-3.562C5.241 1.402 8.238.04 11.835 0c.009 0 .018.004.024.01z" fill="url(#lobe-icons-new-api-1-_R_0_)"></path><path d="M8.721 11.903l2.455-.708.72-2.48a.066.066 0 01.127.002l.58 2.26c.776.437 1.65.755 2.622.956a.05.05 0 01.028.075.05.05 0 01-.024.019l-2.382.709a.163.163 0 00-.109.108l-.72 2.444a.034.034 0 01-.031.027.034.034 0 01-.034-.024l-.713-2.395a.183.183 0 00-.128-.128l-2.39-.705a.084.084 0 01-.044-.13.084.084 0 01.043-.03z" fill="url(#lobe-icons-new-api-2-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-new-api-0-_R_0_" x1="17.889" x2="17.889" y1=".854" y2="24"><stop stop-color="#F85EAD"></stop><stop offset="1" stop-color="#FD75FD"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-new-api-1-_R_0_" x1="5.936" x2="5.936" y1="0" y2="23.146"><stop offset=".332" stop-color="#11F5EF"></stop><stop offset="1" stop-color="#C738FB"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-new-api-2-_R_0_" x1="11.961" x2="11.961" y1="8.666" y2="15.315"><stop offset=".332" stop-color="#11F5EF"></stop><stop offset="1" stop-color="#C738FB"></stop></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
74
mengyaprofile-backend/data/logo/nodejs.svg
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -99 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
|
||||
<defs>
|
||||
|
||||
<linearGradient x1="68.188%" y1="17.487%" x2="27.823%" y2="89.755%" id="b">
|
||||
|
||||
<stop stop-color="#41873F" offset="0%"/>
|
||||
|
||||
<stop stop-color="#418B3D" offset="32.88%"/>
|
||||
|
||||
<stop stop-color="#419637" offset="63.52%"/>
|
||||
|
||||
<stop stop-color="#3FA92D" offset="93.19%"/>
|
||||
|
||||
<stop stop-color="#3FAE2A" offset="100%"/>
|
||||
|
||||
</linearGradient>
|
||||
|
||||
<path id="a" d="M57.903 1.85a5.957 5.957 0 0 0-5.894 0L3.352 29.933c-1.85 1.04-2.89 3.005-2.89 5.085v56.286c0 2.08 1.156 4.045 2.89 5.085l48.657 28.085a5.957 5.957 0 0 0 5.894 0l48.658-28.085c1.849-1.04 2.89-3.005 2.89-5.085V35.019c0-2.08-1.157-4.045-2.89-5.085L57.903 1.85z"/>
|
||||
|
||||
<linearGradient x1="43.277%" y1="55.169%" x2="159.245%" y2="-18.306%" id="d">
|
||||
|
||||
<stop stop-color="#41873F" offset="13.76%"/>
|
||||
|
||||
<stop stop-color="#54A044" offset="40.32%"/>
|
||||
|
||||
<stop stop-color="#66B848" offset="71.36%"/>
|
||||
|
||||
<stop stop-color="#6CC04A" offset="90.81%"/>
|
||||
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient x1="-4413.77%" y1="13.43%" x2="5327.93%" y2="13.43%" id="e">
|
||||
|
||||
<stop stop-color="#6CC04A" offset="9.192%"/>
|
||||
|
||||
<stop stop-color="#66B848" offset="28.64%"/>
|
||||
|
||||
<stop stop-color="#54A044" offset="59.68%"/>
|
||||
|
||||
<stop stop-color="#41873F" offset="86.24%"/>
|
||||
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient x1="-4.389%" y1="49.997%" x2="101.499%" y2="49.997%" id="f">
|
||||
|
||||
<stop stop-color="#6CC04A" offset="9.192%"/>
|
||||
|
||||
<stop stop-color="#66B848" offset="28.64%"/>
|
||||
|
||||
<stop stop-color="#54A044" offset="59.68%"/>
|
||||
|
||||
<stop stop-color="#41873F" offset="86.24%"/>
|
||||
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient x1="-9713.77%" y1="36.21%" x2="27.93%" y2="36.21%" id="g">
|
||||
|
||||
<stop stop-color="#6CC04A" offset="9.192%"/>
|
||||
|
||||
<stop stop-color="#66B848" offset="28.64%"/>
|
||||
|
||||
<stop stop-color="#54A044" offset="59.68%"/>
|
||||
|
||||
<stop stop-color="#41873F" offset="86.24%"/>
|
||||
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient x1="-103.861%" y1="50.275%" x2="100.797%" y2="50.275%" id="h">
|
||||
|
||||
<stop stop-color="#6CC04A" offset="9.192%"/>
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
1
mengyaprofile-backend/data/logo/nvidia.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Nvidia</title><path d="M10.212 8.976V7.62c.127-.01.256-.017.388-.021 3.596-.117 5.957 3.184 5.957 3.184s-2.548 3.647-5.282 3.647a3.227 3.227 0 01-1.063-.175v-4.109c1.4.174 1.681.812 2.523 2.258l1.873-1.627a4.905 4.905 0 00-3.67-1.846 6.594 6.594 0 00-.729.044m0-4.476v2.025c.13-.01.259-.019.388-.024 5.002-.174 8.261 4.226 8.261 4.226s-3.743 4.69-7.643 4.69c-.338 0-.675-.031-1.007-.092v1.25c.278.038.558.057.838.057 3.629 0 6.253-1.91 8.794-4.169.421.347 2.146 1.193 2.501 1.564-2.416 2.083-8.048 3.763-11.24 3.763-.308 0-.603-.02-.894-.048V19.5H24v-15H10.21zm0 9.756v1.068c-3.356-.616-4.287-4.21-4.287-4.21a7.173 7.173 0 014.287-2.138v1.172h-.005a3.182 3.182 0 00-2.502 1.178s.615 2.276 2.507 2.931m-5.961-3.3c1.436-1.935 3.604-3.148 5.961-3.336V6.523C5.81 6.887 2 10.723 2 10.723s2.158 6.427 8.21 7.015v-1.166C5.77 16 4.25 10.958 4.25 10.958h-.002z" fill="#74B71B" fill-rule="nonzero"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
mengyaprofile-backend/data/logo/obsidian.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Obsidian</title><path d="M9.643 14.012c.615-.183 1.605-.465 2.745-.534-.684-1.725-.849-3.235-.716-4.579.153-1.552.7-2.847 1.234-3.95.114-.235.223-.454.328-.664.149-.297.289-.577.42-.86.217-.47.378-.885.46-1.27.08-.38.08-.719-.014-1.044-.095-.325-.297-.675-.681-1.06a1.6 1.6 0 00-1.475.36l-4.95 4.453a1.602 1.602 0 00-.512.952l-.427 2.83c.67.592 2.327 2.317 3.335 4.71.09.213.174.432.253.656zM5.855 9.937c-.024.1-.057.197-.099.29L3.14 16.058a1.602 1.602 0 00.313 1.772l4.117 4.24c2.102-3.102 1.795-6.02.835-8.3-.728-1.73-1.832-3.083-2.55-3.833z" fill="#A88BFA"></path><path d="M8.52 22.57c.073.01.146.018.22.02.781.023 2.095.091 3.16.288.87.16 2.593.642 4.011 1.056 1.082.316 2.197-.548 2.354-1.664.115-.814.33-1.735.725-2.58l-.009.004c-.67-1.87-1.523-3.077-2.417-3.847a5.294 5.294 0 00-2.777-1.258c-1.541-.216-2.952.189-3.841.45.532 2.218.368 4.828-1.425 7.53z" fill="#A88BFA"></path><path d="M19.676 18.538a69.072 69.072 0 001.858-2.952.811.811 0 00-.061-.901c-.516-.684-1.504-2.075-2.042-3.362-.554-1.323-.636-3.378-.64-4.378a1.708 1.708 0 00-.359-1.051L15.235 1.83a3.757 3.757 0 01-.076.545c-.107.503-.307 1.004-.536 1.498-.135.29-.29.601-.446.915-.105.21-.21.42-.31.626-.517 1.068-.998 2.227-1.132 3.59-.125 1.262.046 2.73.814 4.484.128.01.257.025.386.043a6.364 6.364 0 013.327 1.506c.916.79 1.743 1.921 2.414 3.5z" fill="#A88BFA"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
mengyaprofile-backend/data/logo/ollama.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Ollama</title><path d="M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
1
mengyaprofile-backend/data/logo/openai.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
mengyaprofile-backend/data/logo/openclaw.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenClaw</title><path d="M12 2.568c-6.33 0-9.495 5.275-9.495 9.495 0 4.22 3.165 8.44 6.33 9.494v2.11h2.11v-2.11s1.055.422 2.11 0v2.11h2.11v-2.11c3.165-1.055 6.33-5.274 6.33-9.494S18.33 2.568 12 2.568z" fill="url(#lobe-icons-open-claw-0-_R_0_)"></path><path d="M3.56 9.953C.396 8.898-.66 11.008.396 13.118c1.055 2.11 3.164 1.055 4.22-1.055.632-1.477 0-2.11-1.056-2.11z" fill="url(#lobe-icons-open-claw-1-_R_0_)"></path><path d="M20.44 9.953c3.164-1.055 4.22 1.055 3.164 3.165-1.055 2.11-3.164 1.055-4.22-1.055-.632-1.477 0-2.11 1.056-2.11z" fill="url(#lobe-icons-open-claw-2-_R_0_)"></path><path d="M5.507 1.875c.476-.285 1.036-.233 1.615.037.577.27 1.223.774 1.937 1.488a.316.316 0 01-.447.447c-.693-.693-1.279-1.138-1.757-1.361-.475-.222-.795-.205-1.022-.069a.317.317 0 01-.326-.542zM16.877 1.913c.58-.27 1.14-.323 1.616-.038a.317.317 0 01-.326.542c-.227-.136-.547-.153-1.022.069-.478.223-1.064.668-1.756 1.361a.316.316 0 11-.448-.447c.714-.714 1.36-1.218 1.936-1.487z" fill="#FF4D4D"></path><path d="M8.835 9.109a1.266 1.266 0 100-2.532 1.266 1.266 0 000 2.532zM15.165 9.109a1.266 1.266 0 100-2.532 1.266 1.266 0 000 2.532z" fill="#050810"></path><path d="M9.046 8.16a.527.527 0 100-1.056.527.527 0 000 1.055zM15.376 8.16a.527.527 0 100-1.055.527.527 0 000 1.054z" fill="#00E5CC"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-open-claw-0-_R_0_" x1="-.659" x2="27.023" y1=".458" y2="22.855"><stop stop-color="#FF4D4D"></stop><stop offset="1" stop-color="#991B1B"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-open-claw-1-_R_0_" x1="0" x2="4.311" y1="9.672" y2="14.949"><stop stop-color="#FF4D4D"></stop><stop offset="1" stop-color="#991B1B"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-open-claw-2-_R_0_" x1="19.385" x2="24.399" y1="9.953" y2="14.462"><stop stop-color="#FF4D4D"></stop><stop offset="1" stop-color="#991B1B"></stop></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
mengyaprofile-backend/data/logo/opencode.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>opencode</title><path d="M16 6H8v12h8V6zm4 16H4V2h16v20z"></path></svg>
|
||||
|
After Width: | Height: | Size: 235 B |
8
mengyaprofile-backend/data/logo/rabbitmq.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="-0.5 0 257 257" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M245.733754,102.437432 L163.822615,102.437432 C161.095475,102.454639 158.475045,101.378893 156.546627,99.4504749 C154.618208,97.5220567 153.542463,94.901627 153.559669,92.174486 L153.559669,10.2633479 C153.559723,7.54730691 152.476409,4.94343327 150.549867,3.02893217 C148.623325,1.11443107 146.012711,0.0474632135 143.296723,0.0645452326 L112.636172,0.0645452326 C109.920185,0.0474632135 107.30957,1.11443107 105.383029,3.02893217 C103.456487,4.94343327 102.373172,7.54730691 102.373226,10.2633479 L102.373226,92.174486 C102.390432,94.901627 101.314687,97.5220567 99.3862689,99.4504749 C97.4578506,101.378893 94.8374209,102.454639 92.11028,102.437432 L61.4497286,102.437432 C58.7225877,102.454639 56.102158,101.378893 54.1737397,99.4504749 C52.2453215,97.5220567 51.1695761,94.901627 51.1867826,92.174486 L51.1867826,10.2633479 C51.203989,7.5362069 50.1282437,4.91577722 48.1998255,2.98735896 C46.2714072,1.05894071 43.6509775,-0.0168046317 40.9238365,0.000198540275 L10.1991418,0.000198540275 C7.48310085,0.000198540275 4.87922722,1.08366231 2.96472611,3.0102043 C1.05022501,4.93674629 -0.0167428433,7.54736062 0.000135896304,10.2633479 L0.000135896304,245.79796 C-0.0168672756,248.525101 1.05887807,251.14553 2.98729632,253.073949 C4.91571457,255.002367 7.53614426,256.078112 10.2632852,256.061109 L245.733754,256.061109 C248.460895,256.078112 251.081324,255.002367 253.009743,253.073949 C254.938161,251.14553 256.013906,248.525101 255.9967,245.79796 L255.9967,112.892808 C256.066222,110.132577 255.01362,107.462105 253.07944,105.491659 C251.14526,103.521213 248.4948,102.419191 245.733754,102.437432 Z M204.553817,189.4159 C204.570741,193.492844 202.963126,197.408658 200.08629,200.297531 C197.209455,203.186403 193.300387,204.810319 189.223407,204.810319 L168.697515,204.810319 C164.620535,204.810319 160.711467,203.186403 157.834632,200.297531 C154.957796,197.408658 153.350181,193.492844 153.367105,189.4159 L153.367105,168.954151 C153.350181,164.877207 154.957796,160.961393 157.834632,158.07252 C160.711467,155.183648 164.620535,153.559732 168.697515,153.559732 L189.223407,153.559732 C193.300387,153.559732 197.209455,155.183648 200.08629,158.07252 C202.963126,160.961393 204.570741,164.877207 204.553817,168.954151 L204.553817,189.4159 L204.553817,189.4159 Z" fill="#FF6600">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
2
mengyaprofile-backend/data/logo/tailwind.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_tailwind</title><path d="M9,13.7q1.4-5.6,7-5.6c5.6,0,6.3,4.2,9.1,4.9q2.8.7,4.9-2.1-1.4,5.6-7,5.6c-5.6,0-6.3-4.2-9.1-4.9Q11.1,10.9,9,13.7ZM2,22.1q1.4-5.6,7-5.6c5.6,0,6.3,4.2,9.1,4.9q2.8.7,4.9-2.1-1.4,5.6-7,5.6c-5.6,0-6.3-4.2-9.1-4.9Q4.1,19.3,2,22.1Z" style="fill:#44a8b3"/></svg>
|
||||
|
After Width: | Height: | Size: 503 B |
1
mengyaprofile-backend/data/logo/tencentcloud.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>TencentCloud</title><path d="M20.0483 17.1416C19.6945 17.4914 18.987 18.0161 17.7488 18.0161C17.2182 18.0161 16.5991 18.0161 16.3338 18.0161C15.98 18.0161 13.3268 18.0161 10.143 18.0161C12.4424 15.8298 14.3881 13.9932 14.565 13.8183C14.7419 13.6434 15.1841 13.2061 15.6263 12.8563C16.5107 12.0692 17.2182 11.9817 17.8373 11.9817C18.7217 11.9817 19.4292 12.3316 20.0483 12.8563C21.2864 13.9932 21.2864 16.0047 20.0483 17.1416ZM21.5518 11.457C20.6674 10.495 19.3408 9.88281 17.9257 9.88281C16.6875 9.88281 15.6263 10.3201 14.6534 11.0197C14.2997 11.3695 13.769 11.7194 13.3268 12.2441C12.9731 12.5939 5.36719 19.9401 5.36719 19.9401C5.80939 20.0276 6.34003 20.0276 6.78223 20.0276C7.22443 20.0276 16.0685 20.0276 16.4222 20.0276C17.1298 20.0276 17.6604 20.0276 18.191 19.9401C19.3408 19.8527 20.4905 19.4154 21.4633 18.5409C23.4975 16.6168 23.4975 13.381 21.5518 11.457Z" fill="#00A3FF"></path><path d="M9.1701 10.9323C8.19726 10.2326 7.22442 9.88281 6.07469 9.88281C4.65965 9.88281 3.33304 10.495 2.44864 11.457C0.502952 13.4685 0.502952 16.6168 2.53708 18.6283C3.42148 19.4154 4.30589 19.8527 5.36717 19.9401L7.4013 18.0161C7.04754 18.0161 6.60533 18.0161 6.25157 18.0161C5.10185 17.9287 4.39433 17.5789 3.95212 17.1416C2.71396 15.9172 2.71396 13.9932 3.86368 12.7688C4.48277 12.1566 5.19029 11.8943 6.07469 11.8943C6.60533 11.8943 7.4013 11.9817 8.19726 12.7688C8.55102 13.1186 9.52386 13.8183 9.87763 14.1681H9.96607L11.2927 12.8563V12.7688C10.6736 12.1566 9.70075 11.3695 9.1701 10.9323Z" fill="#00C8DC"></path><path d="M18.4564 8.74536C17.4836 6.12171 14.9188 4.28516 12.0003 4.28516C8.5511 4.28516 5.80945 6.82135 5.27881 9.96973C5.54413 9.96973 5.80945 9.88228 6.16321 9.88228C6.51697 9.88228 6.95917 9.96973 7.31294 9.96973C7.75514 7.78336 9.70082 6.20917 12.0003 6.20917C13.946 6.20917 15.6263 7.34608 16.4223 9.00773C16.4223 9.00773 16.5107 9.09518 16.5107 9.00773C17.1298 8.92027 17.8373 8.74536 18.4564 8.74536C18.4564 8.83282 18.4564 8.83282 18.4564 8.74536Z" fill="#006EFF"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
1
mengyaprofile-backend/data/logo/trae.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>TRAE</title><path d="M24 20.541H3.428v-3.426H0V3.4h24V20.54zM3.428 17.115h17.144V6.827H3.428v10.288zm8.573-5.196l-2.425 2.424-2.424-2.424 2.424-2.424 2.425 2.424zm6.857-.001l-2.424 2.423-2.425-2.423 2.425-2.425 2.424 2.425z" fill="#32F08C"></path></svg>
|
||||
|
After Width: | Height: | Size: 377 B |
8
mengyaprofile-backend/data/logo/typescript.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="TypeScript" role="img"
|
||||
viewBox="0 0 512 512"><rect
|
||||
width="512" height="512"
|
||||
rx="15%"
|
||||
fill="#3178c6"/><path fill="#ffffff"
|
||||
d="m233 284h64v-41H118v41h64v183h51zm84 173c8.1 4.2 18 7.3 29 9.4s23 3.1 35 3.1c12 0 23-1.1 34-3.4c11-2.3 20-6.1 28-11c8.1-5.3 15-12 19-21s7.1-19 7.1-32c0-9.1-1.4-17-4.1-24s-6.6-13-12-18c-5.1-5.3-11-10-18-14s-15-8.2-24-12c-6.6-2.7-12-5.3-18-7.9c-5.2-2.6-9.7-5.2-13-7.8c-3.7-2.7-6.5-5.5-8.5-8.4c-2-3-3-6.3-3-10c0-3.4.89-6.5 2.7-9.3s4.3-5.1 7.5-7.1c3.2-2 7.2-3.5 12-4.6c4.7-1.1 9.9-1.6 16-1.6c4.2 0 8.6.31 13 .94c4.6.63 9.3 1.6 14 2.9c4.7 1.3 9.3 2.9 14 4.9c4.4 2 8.5 4.3 12 6.9v-47c-7.6-2.9-16-5.1-25-6.5s-19-2.1-31-2.1c-12 0-23 1.3-34 3.8s-20 6.5-28 12c-8.1 5.4-14 12-19 21c-4.7 8.4-7 18-7 30c0 15 4.3 28 13 38c8.6 11 22 19 39 27c6.9 2.8 13 5.6 19 8.3s11 5.5 15 8.4c4.3 2.9 7.7 6.1 10 9.5c2.5 3.4 3.8 7.4 3.8 12c0 3.2-.78 6.2-2.3 9s-3.9 5.2-7.1 7.2s-7.1 3.6-12 4.8c-4.7 1.1-10 1.7-17 1.7c-11 0-22-1.9-32-5.7c-11-3.8-21-9.5-28.1-15.44z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
mengyaprofile-backend/data/logo/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Vercel</title><path d="M12 0l12 20.785H0L12 0z"></path></svg>
|
||||
|
After Width: | Height: | Size: 225 B |
2
mengyaprofile-backend/data/logo/vite.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" fill="none"><title>file_type_vite</title><path d="M29.8836 6.146L16.7418 29.6457c-.2714.4851-.9684.488-1.2439.0052L2.0956 6.1482c-.3-.5262.1498-1.1635.746-1.057l13.156 2.3516a.7144.7144 0 00.2537-.0004l12.8808-2.3478c.5942-.1083 1.0463.5241.7515 1.0513z" fill="url(#paint0_linear)"/><path d="M22.2644 2.0069l-9.7253 1.9056a.3571.3571 0 00-.2879.3294l-.5982 10.1038c-.014.238.2045.4227.4367.3691l2.7077-.6248c.2534-.0585.4823.1647.4302.4194l-.8044 3.9393c-.0542.265.1947.4918.4536.4132l1.6724-.5082c.2593-.0787.5084.1487.4536.414l-1.2784 6.1877c-.08.387.4348.598.6495.2662L16.5173 25 24.442 9.1848c.1327-.2648-.096-.5667-.387-.5106l-2.787.5379c-.262.0505-.4848-.1934-.4109-.4497l1.8191-6.306c.074-.2568-.1496-.5009-.4118-.4495z" fill="url(#paint1_linear)"/><defs id="defs50"><linearGradient id="paint0_linear" x1="6.0002" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.07142 0 0 .07142 1.3398 1.8944)"><stop stop-color="#41D1FF" id="stop38"/><stop offset="1" stop-color="#BD34FE" id="stop40"/></linearGradient><linearGradient id="paint1_linear" x1="194.651" y1="8.8182" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.07142 0 0 .07142 1.3398 1.8944)"><stop stop-color="#FFEA83" id="stop43"/><stop offset=".0833" stop-color="#FFDD35" id="stop45"/><stop offset="1" stop-color="#FFA800" id="stop47"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
2
mengyaprofile-backend/data/logo/vscode.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_vscode</title><path d="M29.01,5.03,23.244,2.254a1.742,1.742,0,0,0-1.989.338L2.38,19.8A1.166,1.166,0,0,0,2.3,21.447c.025.027.05.053.077.077l1.541,1.4a1.165,1.165,0,0,0,1.489.066L28.142,5.75A1.158,1.158,0,0,1,30,6.672V6.605A1.748,1.748,0,0,0,29.01,5.03Z" style="fill:#0065a9"/><path d="M29.01,26.97l-5.766,2.777a1.745,1.745,0,0,1-1.989-.338L2.38,12.2A1.166,1.166,0,0,1,2.3,10.553c.025-.027.05-.053.077-.077l1.541-1.4A1.165,1.165,0,0,1,5.41,9.01L28.142,26.25A1.158,1.158,0,0,0,30,25.328V25.4A1.749,1.749,0,0,1,29.01,26.97Z" style="fill:#007acc"/><path d="M23.244,29.747a1.745,1.745,0,0,1-1.989-.338A1.025,1.025,0,0,0,23,28.684V3.316a1.024,1.024,0,0,0-1.749-.724,1.744,1.744,0,0,1,1.989-.339l5.765,2.772A1.748,1.748,0,0,1,30,6.6V25.4a1.748,1.748,0,0,1-.991,1.576Z" style="fill:#1f9cf0"/></svg>
|
||||
|
After Width: | Height: | Size: 1014 B |
1
mengyaprofile-backend/data/logo/workersai.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>WorkersAI</title><path clip-rule="evenodd" d="M15.99 2.444h-2.135v4.69l2.134.006V2.444zM11.06 5.153l2.224 2.225L11.77 8.88 9.552 6.662l1.51-1.51zM6.845 9.455h4.696l-.007 2.133h-4.69V9.456zm2.71 4.928l2.222-2.224 1.505 1.514-2.218 2.217-1.51-1.509.001.002zm4.3 4.216v-4.696l2.134.007v4.69h-2.134zm4.928-2.706l-2.225-2.225 1.514-1.504 2.22 2.22-1.51 1.51h.001zM23 11.588h-4.696l.007-2.133H23v2.133zm-2.709-4.926l-2.223 2.223-1.504-1.513 2.22-2.22 1.507 1.51zM3.2 2.926V4.13H1.994v1.929H3.2v1.204h1.927V6.059h1.204V4.131H5.127V2.926H3.2zm0 18.835v-2.2H1v-1.927h2.2v-2.198h1.927v2.198h2.2v1.927h-2.2v2.2H3.2z" fill="#F38020" fill-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 778 B |
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
{
|
||||
"projects": [
|
||||
{
|
||||
"title": "萌芽主页",
|
||||
"description": "一个简洁美观的个人主页,展示个人信息和项目",
|
||||
"link": "https://shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2025/11/03/690836f3c87b2.png",
|
||||
"tags": ["个人主页","React","Flask"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":true
|
||||
},
|
||||
{
|
||||
"title": "萌芽笔记",
|
||||
"description": "展示自己学习过程中的一些MarkDown笔记",
|
||||
"link": "https://note.shumengya.top",
|
||||
"icon": "https://image.shumengya.top/i/2025/11/02/mengyanote.png",
|
||||
"tags": [ "笔记","Obsidion","React","Flask"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":true
|
||||
},
|
||||
{
|
||||
"title": "萌芽作品集",
|
||||
"description": "展示个人制作的一些小创意和小项目",
|
||||
"link": "https://work.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2025/11/02/69074f8f5ed5e.png",
|
||||
"tags": [ "作品集","React","Flask"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":true
|
||||
},
|
||||
{
|
||||
"title": "万象口袋",
|
||||
"description": "一款跨平台的聚合式软件",
|
||||
"link": "https://infogenie.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2025/11/02/6907516fb77d5.png",
|
||||
"tags": [ "聚合","工具","React","Flask"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":true
|
||||
},
|
||||
{
|
||||
"title": "萌芽农场",
|
||||
"description": "一款2D平台联机农场经营游戏",
|
||||
"link": "https://work.shumengya.top/#/work/mengyafarm",
|
||||
"icon": "https://img.shumengya.top/i/2025/11/02/6907599cbaf10.png",
|
||||
"tags": [ "农场","游戏","联机","Godot","Python"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":true
|
||||
},
|
||||
{
|
||||
"title": "萌芽短链",
|
||||
"description": "一个轻量简单的短链跳转系统",
|
||||
"link": "https://short.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2025/11/17/691ad6c78283a.png",
|
||||
"tags": [ "短链接","React","Flask"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":true
|
||||
},
|
||||
{
|
||||
"title": "萌芽监控面板",
|
||||
"description": "一个轻量简单的监控系统",
|
||||
"link": "https://monitor.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/12/6964fffd54221.png",
|
||||
"tags": [ "监控","React","Golang"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":true
|
||||
},
|
||||
{
|
||||
"title": "萌芽Ping",
|
||||
"description": "网站监控系统",
|
||||
"link": "https://ping.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/22/6971ad8eb9e56.png",
|
||||
"tags": [ "监控","React","Gin"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":true
|
||||
},
|
||||
{
|
||||
"title": "萌芽密码管理器",
|
||||
"description": "自制密码库,做的太烂,我都不怎么用(",
|
||||
"link": "https://keyvault.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2026/02/10/698b2f4c321ec.ico",
|
||||
"tags": [ "监控","React","Gin"],
|
||||
"admin":true,
|
||||
"show":true,
|
||||
"develop":true
|
||||
},
|
||||
{
|
||||
"title": "萌芽漂流瓶",
|
||||
"description": "匿名漂流瓶系统",
|
||||
"link": "https://bottle.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2025/11/17/691ad6cc635d2.png",
|
||||
"tags": [ "监控","React","Flask"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":true
|
||||
},
|
||||
|
||||
|
||||
|
||||
{
|
||||
"title": "萌芽盘",
|
||||
"description": "一个轻量级在线网盘,支持文件上传、下载、分享等功能",
|
||||
"link": "https://pan.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/04/695a660870959.png",
|
||||
"tags": ["网盘","OpenList"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"title": "编程速查表[CF]",
|
||||
"description": "一个编程入门速查表",
|
||||
"link": "https://reference.smyhub.com",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/12/6964fd1159f91.png",
|
||||
"tags": [ "QuickReference","编程速查表"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "在线思维导图[CF]",
|
||||
"description": "一个思维导图工具",
|
||||
"link": "https://mind-map.smyhub.com",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/12/6964fd1271081.png",
|
||||
"tags": [ "mindmap","思维导图"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "it-tools工具集[CF]",
|
||||
"description": "一个轻量简单的编程工具集合",
|
||||
"link": "https://it-tools.smyhub.com",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/12/6964fd1229dc3.png",
|
||||
"tags": [ "it-tools","工具集合"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "xtools工具集[CF]",
|
||||
"description": "一个轻量简单的编程工具集合",
|
||||
"link": "https://xtools.smyhub.com",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/12/6964fd133e934.png",
|
||||
"tags": [ "xtool","工具集合"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "萌芽备忘录",
|
||||
"description": "一个轻量简单的备忘录系统",
|
||||
"link": "https://memos.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/13/69663c9f1acc1.png",
|
||||
"tags": [ "memos","备忘录"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "网页魔方[CF]",
|
||||
"description": "就是一个简单的网页魔方",
|
||||
"link": "https://cube.smyhub.com",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/14/69678bf409a31.webp",
|
||||
"tags": [ "web-cube","网页魔方"],
|
||||
"admin":false,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{
|
||||
"title": "大萌芽1Panel面板[WG]",
|
||||
"description": "大萌芽1panel面板后台",
|
||||
"link": "http://10.0.0.233:19132/smy",
|
||||
"icon": "https://img.shumengya.top/i/2025/11/02/69076213d9200.webp",
|
||||
"tags": [ "1Panel","面板"],
|
||||
"admin":true,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "小萌芽1Panel面板[WG]",
|
||||
"description": "小萌芽1panel面板后台",
|
||||
"link": "http://10.0.0.100:19132/smy",
|
||||
"icon": "https://img.shumengya.top/i/2025/11/02/69076213d9200.webp",
|
||||
"tags": [ "1Panel","面板"],
|
||||
"admin":true,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "easytier面板",
|
||||
"description": "阿里云成都easytier面板后台",
|
||||
"link": "https://easytier.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/12/6964fd1307fdc.png",
|
||||
"tags": [ "easytier","面板"],
|
||||
"admin":true,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "WireGuard面板[等待复活]",
|
||||
"description": "阿里云成都WireGuard面板后台",
|
||||
"link": "https://wireguard.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/12/6964fd12baa3c.png",
|
||||
"tags": [ "WireGuard","面板"],
|
||||
"admin":true,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "大萌芽Portainer面板[WG]",
|
||||
"description": "大萌芽Portainer面板后台",
|
||||
"link": "http://10.0.0.233:8484",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/12/6964fd119f8d1.png",
|
||||
"tags": [ "Portainer","面板"],
|
||||
"admin":true,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "小萌芽Portainer面板[WG]",
|
||||
"description": "小萌芽Portainer面板后台",
|
||||
"link": "https://10.0.0.100:8484",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/12/6964fd119f8d1.png",
|
||||
"tags": [ "Portainer","面板"],
|
||||
"admin":true,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "萌芽Docker镜像仓库[等待复活]",
|
||||
"description": "萌芽Docker镜像仓库后台",
|
||||
"link": "https://repo.docker.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2025/11/02/6907621448bac.png",
|
||||
"tags": [ "Docker","镜像仓库"],
|
||||
"admin":true,
|
||||
"show":true,
|
||||
"develop":false
|
||||
},
|
||||
{
|
||||
"title": "萌芽通知",
|
||||
"description": "萌芽通知后台",
|
||||
"link": "https://notice.shumengya.top",
|
||||
"icon": "https://img.shumengya.top/i/2026/01/12/6964ff8787011.png",
|
||||
"tags": [ "通知","gotify"],
|
||||
"admin":true,
|
||||
"show":true,
|
||||
"develop":false
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
{
|
||||
"title": "技术栈",
|
||||
"items": [
|
||||
{
|
||||
"name": "Android",
|
||||
"link": "https://developer.android.com/",
|
||||
"svg": "android.svg",
|
||||
"color": "#3DDC84",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"name": "C#",
|
||||
"link": "https://learn.microsoft.com/en-us/dotnet/csharp/",
|
||||
"svg": "csharp.svg",
|
||||
"color": "#68217A",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Docker",
|
||||
"link": "https://www.docker.com/",
|
||||
"svg": "docker.svg",
|
||||
"color": "#2496ED",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Flask",
|
||||
"link": "https://flask.palletsprojects.com/",
|
||||
"svg": "flask.svg",
|
||||
"color": "#000000",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Flutter",
|
||||
"link": "https://flutter.dev/",
|
||||
"svg": "flutter.svg",
|
||||
"color": "#02569B",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"name": "Gin",
|
||||
"link": "https://gin-gonic.com/",
|
||||
"svg": "gin.svg",
|
||||
"color": "#00ADD8",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Git",
|
||||
"link": "https://git-scm.com/",
|
||||
"svg": "git.svg",
|
||||
"color": "#F05032",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Godot",
|
||||
"link": "https://godotengine.org/",
|
||||
"svg": "godot.svg",
|
||||
"color": "#478CBF",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Golang",
|
||||
"link": "https://go.dev/",
|
||||
"svg": "golang.svg",
|
||||
"color": "#00ADD8",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Java",
|
||||
"link": "https://www.oracle.com/java/",
|
||||
"svg": "java.svg",
|
||||
"color": "#ED8B00",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "JavaScript",
|
||||
"link": "https://developer.mozilla.org/en-US/docs/Web/JavaScript",
|
||||
"svg": "javascript.svg",
|
||||
"color": "#F7DF1E",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"name": "JSON",
|
||||
"link": "https://www.json.org/",
|
||||
"svg": "json.svg",
|
||||
"color": "#000000",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"name": "Linux",
|
||||
"link": "https://www.linux.org/",
|
||||
"svg": "linux.svg",
|
||||
"color": "#FCC624",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Markdown",
|
||||
"link": "https://daringfireball.net/projects/markdown/",
|
||||
"svg": "markdown.svg",
|
||||
"color": "#000000",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"name": "Minecraft",
|
||||
"link": "https://www.minecraft.net/",
|
||||
"svg": "minecraft.svg",
|
||||
"color": "#62B74A",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"name": "MongoDB",
|
||||
"link": "https://www.mongodb.com/",
|
||||
"svg": "mongodb.svg",
|
||||
"color": "#47A248",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "MySQL",
|
||||
"link": "https://www.mysql.com/",
|
||||
"svg": "mysql.svg",
|
||||
"color": "#00758F",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Node.js",
|
||||
"link": "https://nodejs.org/",
|
||||
"svg": "nodejs.svg",
|
||||
"color": "#339933",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"name": "PostgreSQL",
|
||||
"link": "https://www.postgresql.org/",
|
||||
"svg": "postgres.svg",
|
||||
"color": "#4169E1",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Python",
|
||||
"link": "https://www.python.org/",
|
||||
"svg": "python.svg",
|
||||
"color": "#3776AB",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "React",
|
||||
"link": "https://react.dev/",
|
||||
"svg": "react.svg",
|
||||
"color": "#61DAFB",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Redis",
|
||||
"link": "https://redis.io/",
|
||||
"svg": "redis.svg",
|
||||
"color": "#DC382D",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Spring",
|
||||
"link": "https://spring.io/",
|
||||
"svg": "spring.svg",
|
||||
"color": "#6DB33F",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Unity",
|
||||
"link": "https://unity.com/",
|
||||
"svg": "unity.svg",
|
||||
"color": "#FFFFFF",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "Vue",
|
||||
"link": "https://vuejs.org/",
|
||||
"svg": "vue.svg",
|
||||
"color": "#4FC08D",
|
||||
"show": false
|
||||
},
|
||||
{
|
||||
"name": "WireGuard",
|
||||
"link": "https://www.wireguard.com/",
|
||||
"svg": "wireguard.svg",
|
||||
"color": "#a10808",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"name": "SQLite",
|
||||
"link": "https://sqlite.org",
|
||||
"svg": "sqlite.svg",
|
||||
"color": "#0671c8",
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
{
|
||||
"title": "技术栈",
|
||||
"items": [
|
||||
{
|
||||
"name": "Android",
|
||||
"icon": "https://img.shields.io/badge/-Android-3DDC84?style=flat&logo=android&logoColor=white",
|
||||
"link": "https://developer.android.com/"
|
||||
},
|
||||
{
|
||||
"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": "Docker",
|
||||
"icon": "https://img.shields.io/badge/-Docker-2496ED?style=flat&logo=docker&logoColor=white",
|
||||
"link": "https://www.docker.com/"
|
||||
},
|
||||
{
|
||||
"name": "Flask",
|
||||
"icon": "https://img.shields.io/badge/-Flask-000000?style=flat&logo=flask&logoColor=white",
|
||||
"link": "https://flask.palletsprojects.com/"
|
||||
},
|
||||
{
|
||||
"name": "Flutter",
|
||||
"icon": "https://img.shields.io/badge/-Flutter-02569B?style=flat&logo=flutter&logoColor=white",
|
||||
"link": "https://flutter.dev/"
|
||||
},
|
||||
{
|
||||
"name": "Gin",
|
||||
"icon": "https://img.shields.io/badge/-Gin-00ADD8?style=flat&logo=go&logoColor=white",
|
||||
"link": "https://gin-gonic.com/"
|
||||
},
|
||||
{
|
||||
"name": "Git",
|
||||
"icon": "https://img.shields.io/badge/-Git-F05032?style=flat&logo=git&logoColor=white",
|
||||
"link": "https://git-scm.com/"
|
||||
},
|
||||
{
|
||||
"name": "Godot",
|
||||
"icon": "https://img.shields.io/badge/-Godot-478CBF?style=flat&logo=godot&logoColor=white",
|
||||
"link": "https://godotengine.org/"
|
||||
},
|
||||
{
|
||||
"name": "Golang",
|
||||
"icon": "https://img.shields.io/badge/-Golang-00ADD8?style=flat&logo=go&logoColor=white",
|
||||
"link": "https://go.dev/"
|
||||
},
|
||||
{
|
||||
"name": "Java",
|
||||
"icon": "https://img.shields.io/badge/-Java-007396?style=flat&logo=java&logoColor=white",
|
||||
"link": "https://www.oracle.com/java/"
|
||||
},
|
||||
{
|
||||
"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": "JSON",
|
||||
"icon": "https://img.shields.io/badge/-JSON-000000?style=flat&logo=json&logoColor=white",
|
||||
"link": "https://www.json.org/"
|
||||
},
|
||||
{
|
||||
"name": "Linux",
|
||||
"icon": "https://img.shields.io/badge/-Linux-000000?style=flat&logo=linux&logoColor=white",
|
||||
"link": "https://www.linux.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/"
|
||||
},
|
||||
{
|
||||
"name": "MongoDB",
|
||||
"icon": "https://img.shields.io/badge/-MongoDB-47A248?style=flat&logo=mongodb&logoColor=white",
|
||||
"link": "https://www.mongodb.com/"
|
||||
},
|
||||
{
|
||||
"name": "MySQL",
|
||||
"icon": "https://img.shields.io/badge/-MySQL-00758F?style=flat&logo=mysql&logoColor=white",
|
||||
"link": "https://www.mysql.com/"
|
||||
},
|
||||
{
|
||||
"name": "Node.js",
|
||||
"icon": "https://img.shields.io/badge/-Node.js-339933?style=flat&logo=nodedotjs&logoColor=white",
|
||||
"link": "https://nodejs.org/"
|
||||
},
|
||||
{
|
||||
"name": "PostgreSQL",
|
||||
"icon": "https://img.shields.io/badge/-PostgreSQL-4169E1?style=flat&logo=postgresql&logoColor=white",
|
||||
"link": "https://www.postgresql.org/"
|
||||
},
|
||||
{
|
||||
"name": "Python",
|
||||
"icon": "https://img.shields.io/badge/-Python-3776AB?style=flat&logo=python&logoColor=white",
|
||||
"link": "https://www.python.org/"
|
||||
},
|
||||
{
|
||||
"name": "React",
|
||||
"icon": "https://img.shields.io/badge/-React-20232A?style=flat&logo=react&logoColor=61DAFB",
|
||||
"link": "https://react.dev/"
|
||||
},
|
||||
{
|
||||
"name": "Redis",
|
||||
"icon": "https://img.shields.io/badge/-Redis-DC382D?style=flat&logo=redis&logoColor=white",
|
||||
"link": "https://redis.io/"
|
||||
},
|
||||
{
|
||||
"name": "Spring",
|
||||
"icon": "https://img.shields.io/badge/-Spring-6DB33F?style=flat&logo=spring&logoColor=white",
|
||||
"link": "https://spring.io/"
|
||||
},
|
||||
{
|
||||
"name": "Unity",
|
||||
"icon": "https://img.shields.io/badge/-Unity-000000?style=flat&logo=unity&logoColor=white",
|
||||
"link": "https://unity.com/"
|
||||
}
|
||||
]
|
||||
}
|
||||
618
mengyaprofile-backend/db.py
Normal file
@@ -0,0 +1,618 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import unicodedata
|
||||
from contextlib import contextmanager
|
||||
|
||||
DATA_DIR = os.environ.get('DATA_DIR', os.path.join(os.path.dirname(__file__), 'data'))
|
||||
DATABASE_PATH = os.environ.get('DATABASE_PATH', os.path.join(DATA_DIR, 'site.db'))
|
||||
_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), 'schema.sql')
|
||||
|
||||
|
||||
def _new_connection():
|
||||
os.makedirs(os.path.dirname(DATABASE_PATH) or '.', exist_ok=True)
|
||||
conn = sqlite3.connect(DATABASE_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute('PRAGMA foreign_keys = ON')
|
||||
conn.execute('PRAGMA journal_mode = WAL')
|
||||
return conn
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_connection():
|
||||
conn = _new_connection()
|
||||
try:
|
||||
yield conn
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _apply_schema(conn):
|
||||
with open(_SCHEMA_PATH, 'r', encoding='utf-8') as f:
|
||||
conn.executescript(f.read())
|
||||
|
||||
|
||||
def _profile_is_empty(conn):
|
||||
row = conn.execute('SELECT COUNT(*) AS c FROM profile').fetchone()
|
||||
return row['c'] == 0
|
||||
|
||||
|
||||
def _clean_tag_list(tags):
|
||||
out = []
|
||||
for t in tags or []:
|
||||
s = str(t).strip()
|
||||
if s:
|
||||
out.append(s)
|
||||
return out
|
||||
|
||||
|
||||
def _tech_name_set(conn):
|
||||
return {
|
||||
str(r['name']).strip().lower()
|
||||
for r in conn.execute('SELECT name FROM techstack_item')
|
||||
if r['name'] and str(r['name']).strip()
|
||||
}
|
||||
|
||||
|
||||
def _split_merged_tags(conn, merged):
|
||||
tech_set = _tech_name_set(conn)
|
||||
cat, tech = [], []
|
||||
for t in merged or []:
|
||||
s = str(t).strip()
|
||||
if not s:
|
||||
continue
|
||||
(tech if s.lower() in tech_set else cat).append(s)
|
||||
return cat, tech
|
||||
|
||||
|
||||
def _project_tag_payload_from_body(conn, data):
|
||||
if 'techTags' in data:
|
||||
return _clean_tag_list(data.get('tags')), _clean_tag_list(data.get('techTags'))
|
||||
return _split_merged_tags(conn, data.get('tags') or [])
|
||||
|
||||
|
||||
def _replace_project_tags(conn, project_id, tags):
|
||||
conn.execute('DELETE FROM project_tag WHERE project_id = ?', (project_id,))
|
||||
for j, tag in enumerate(_clean_tag_list(tags)):
|
||||
conn.execute(
|
||||
'INSERT INTO project_tag (project_id, sort_order, tag) VALUES (?, ?, ?)',
|
||||
(project_id, j, tag),
|
||||
)
|
||||
|
||||
|
||||
def _replace_project_tech_tags(conn, project_id, tags):
|
||||
conn.execute('DELETE FROM project_tech_tag WHERE project_id = ?', (project_id,))
|
||||
for j, tag in enumerate(_clean_tag_list(tags)):
|
||||
conn.execute(
|
||||
'INSERT INTO project_tech_tag (project_id, sort_order, tag) VALUES (?, ?, ?)',
|
||||
(project_id, j, tag),
|
||||
)
|
||||
|
||||
|
||||
def _migrate_legacy_project_tags_if_needed(conn):
|
||||
tech_set = _tech_name_set(conn)
|
||||
for row in conn.execute('SELECT id FROM project'):
|
||||
pid = row['id']
|
||||
if conn.execute(
|
||||
'SELECT 1 FROM project_tech_tag WHERE project_id = ? LIMIT 1',
|
||||
(pid,),
|
||||
).fetchone():
|
||||
continue
|
||||
tag_rows = conn.execute(
|
||||
'SELECT tag FROM project_tag WHERE project_id = ? ORDER BY sort_order, id',
|
||||
(pid,),
|
||||
).fetchall()
|
||||
if not tag_rows:
|
||||
continue
|
||||
cat, tech = [], []
|
||||
for r in tag_rows:
|
||||
s = str(r['tag']).strip()
|
||||
if not s:
|
||||
continue
|
||||
if s.lower() in tech_set:
|
||||
tech.append(s)
|
||||
else:
|
||||
cat.append(s)
|
||||
if not tech:
|
||||
continue
|
||||
conn.execute('DELETE FROM project_tag WHERE project_id = ?', (pid,))
|
||||
for j, t in enumerate(cat):
|
||||
conn.execute(
|
||||
'INSERT INTO project_tag (project_id, sort_order, tag) VALUES (?,?,?)',
|
||||
(pid, j, t),
|
||||
)
|
||||
for j, t in enumerate(tech):
|
||||
conn.execute(
|
||||
'INSERT INTO project_tech_tag (project_id, sort_order, tag) VALUES (?,?,?)',
|
||||
(pid, j, t),
|
||||
)
|
||||
|
||||
|
||||
def _seed_minimal_defaults(conn):
|
||||
"""空库时写入 profile 与技术栈区块占位行,内容通过管理后台或 SQL 维护。"""
|
||||
conn.execute('INSERT INTO profile (id, nickname) VALUES (1, ?)', ('',))
|
||||
conn.execute('INSERT INTO techstack_section (id, title) VALUES (1, ?)', ('技术栈',))
|
||||
|
||||
|
||||
def _ensure_builtin_tech_items(conn):
|
||||
"""内置技术栈项:按名称幂等追加(无则插入)。"""
|
||||
builtin = (
|
||||
(
|
||||
'Vite',
|
||||
'https://vitejs.dev/',
|
||||
'https://pan.shumengya.top/d/smy/image/devkit/vite.svg',
|
||||
'#646CFF',
|
||||
),
|
||||
(
|
||||
'Tailwind CSS',
|
||||
'https://tailwindcss.com/',
|
||||
'https://pan.shumengya.top/d/smy/image/devkit/tailwind.svg',
|
||||
'#06B6D4',
|
||||
),
|
||||
(
|
||||
'RabbitMQ',
|
||||
'https://www.rabbitmq.com/',
|
||||
'https://pan.shumengya.top/d/smy/image/devkit/rabbitmq.svg',
|
||||
'#FF6600',
|
||||
),
|
||||
)
|
||||
for name, link, svg, color in builtin:
|
||||
if conn.execute(
|
||||
'SELECT 1 FROM techstack_item WHERE name = ? LIMIT 1',
|
||||
(name,),
|
||||
).fetchone():
|
||||
continue
|
||||
m = conn.execute('SELECT COALESCE(MAX(sort_order), 0) AS m FROM techstack_item').fetchone()['m']
|
||||
sort_order = int(m) + 10
|
||||
conn.execute(
|
||||
'''INSERT INTO techstack_item (sort_order, name, link, svg, color, is_show)
|
||||
VALUES (?, ?, ?, ?, ?, ?)''',
|
||||
(sort_order, name, link, svg, color, 1),
|
||||
)
|
||||
|
||||
|
||||
def _ensure_contact_is_show_column(conn):
|
||||
"""旧库 contact 表无 is_show 时追加列(默认展示)。"""
|
||||
cols = {str(r['name']) for r in conn.execute('PRAGMA table_info(contact)').fetchall()}
|
||||
if cols and 'is_show' not in cols:
|
||||
conn.execute(
|
||||
'ALTER TABLE contact ADD COLUMN is_show INTEGER NOT NULL DEFAULT 1'
|
||||
)
|
||||
|
||||
|
||||
def init_storage():
|
||||
with get_connection() as conn:
|
||||
_apply_schema(conn)
|
||||
_ensure_contact_is_show_column(conn)
|
||||
if _profile_is_empty(conn):
|
||||
_seed_minimal_defaults(conn)
|
||||
else:
|
||||
_migrate_legacy_project_tags_if_needed(conn)
|
||||
_ensure_builtin_tech_items(conn)
|
||||
_apply_tech_item_display_patches(conn)
|
||||
_purge_retired_tech_labels(conn)
|
||||
|
||||
|
||||
def _normalize_tech_label_key(s):
|
||||
"""仅保留字母数字并小写;NFKC 合并全角/兼容字形,匹配 d1SQL 各类写法。"""
|
||||
if s is None:
|
||||
return ''
|
||||
t = unicodedata.normalize('NFKC', str(s)).strip()
|
||||
return ''.join(c.lower() for c in t if c.isalnum())
|
||||
|
||||
|
||||
def _purge_retired_tech_labels(conn):
|
||||
"""移除已废弃的技术名:项目标签 + 技术栈表(幂等)。"""
|
||||
retired_keys = {'d1sql'}
|
||||
for table in ('project_tech_tag', 'project_tag'):
|
||||
rows = conn.execute(f'SELECT id, tag FROM {table}').fetchall()
|
||||
for r in rows:
|
||||
if _normalize_tech_label_key(r['tag']) in retired_keys:
|
||||
conn.execute(f'DELETE FROM {table} WHERE id = ?', (r['id'],))
|
||||
rows = conn.execute('SELECT id, name FROM techstack_item').fetchall()
|
||||
for r in rows:
|
||||
if _normalize_tech_label_key(r['name']) in retired_keys:
|
||||
conn.execute('DELETE FROM techstack_item WHERE id = ?', (r['id'],))
|
||||
|
||||
|
||||
def _apply_tech_item_display_patches(conn):
|
||||
"""微调技术栈徽章底色(图标为自带颜色的 SVG 时避免与底色撞色)。"""
|
||||
patches = (
|
||||
# Docker.svg 为蓝色填充:深蓝→亮蓝渐变,与白字、鲸鱼标层次更清晰
|
||||
(
|
||||
'Docker',
|
||||
'linear-gradient(135deg, #052e4a 0%, #0d47a1 42%, #1e88e5 100%)',
|
||||
),
|
||||
# Flask.svg 为 #3babc3 青色,避免纯黑底;深青绿过渡到品牌青
|
||||
(
|
||||
'Flask',
|
||||
'linear-gradient(135deg, #0f3a40 0%, #1e6570 45%, #2d8fa0 100%)',
|
||||
),
|
||||
# JSON / Markdown:Simple Icons 等品牌资料中的标准色(实心)
|
||||
# JSON: https://simpleicons.org — #000000(与 Crockford JSON 标识一致)
|
||||
('JSON', '#000000'),
|
||||
# Markdown: Markdown 标识常用主色 — #083FA5
|
||||
('Markdown', '#083FA5'),
|
||||
)
|
||||
for name, color in patches:
|
||||
conn.execute(
|
||||
'UPDATE techstack_item SET color = ? WHERE name = ?',
|
||||
(color, name),
|
||||
)
|
||||
|
||||
|
||||
def _row_to_profile(row):
|
||||
if row is None:
|
||||
return None
|
||||
keys = ('favicon', 'nickname', 'avatar', 'introduction', 'footer', 'site', 'homepage')
|
||||
return {k: row[k] for k in keys if row[k] is not None}
|
||||
|
||||
|
||||
def get_profile():
|
||||
with get_connection() as conn:
|
||||
row = conn.execute(
|
||||
'''SELECT favicon, nickname, avatar, introduction, footer, site, homepage
|
||||
FROM profile WHERE id = 1'''
|
||||
).fetchone()
|
||||
return _row_to_profile(row)
|
||||
|
||||
|
||||
def get_projects():
|
||||
with get_connection() as conn:
|
||||
rows = conn.execute(
|
||||
'SELECT id, title, description, link, icon, is_admin, is_show, is_develop FROM project ORDER BY sort_order, id'
|
||||
).fetchall()
|
||||
out = []
|
||||
for row in rows:
|
||||
tags = [
|
||||
r['tag']
|
||||
for r in conn.execute(
|
||||
'SELECT tag FROM project_tag WHERE project_id = ? ORDER BY sort_order, id',
|
||||
(row['id'],),
|
||||
)
|
||||
]
|
||||
tech_tags = [
|
||||
r['tag']
|
||||
for r in conn.execute(
|
||||
'SELECT tag FROM project_tech_tag WHERE project_id = ? ORDER BY sort_order, id',
|
||||
(row['id'],),
|
||||
)
|
||||
]
|
||||
out.append({
|
||||
'id': row['id'],
|
||||
'title': row['title'],
|
||||
'description': row['description'] or '',
|
||||
'link': row['link'] or '',
|
||||
'icon': row['icon'] or '',
|
||||
'tags': tags,
|
||||
'techTags': tech_tags,
|
||||
'admin': bool(row['is_admin']),
|
||||
'show': bool(row['is_show']),
|
||||
'develop': bool(row['is_develop']),
|
||||
})
|
||||
return {'projects': out}
|
||||
|
||||
|
||||
def _contact_row_to_dict(row):
|
||||
return {
|
||||
'id': row['id'],
|
||||
'type': row['type'],
|
||||
'label': row['label'] or '',
|
||||
'value': row['value'] or '',
|
||||
'link': row['link'] or '',
|
||||
'icon': row['icon'] or '',
|
||||
'show': bool(row['is_show']),
|
||||
}
|
||||
|
||||
|
||||
def get_contacts():
|
||||
"""前台公开接口:仅返回首页展示的联系方式。"""
|
||||
with get_connection() as conn:
|
||||
rows = conn.execute(
|
||||
'''SELECT id, type, label, value, link, icon, is_show FROM contact
|
||||
WHERE COALESCE(is_show, 1) = 1
|
||||
ORDER BY sort_order, id'''
|
||||
).fetchall()
|
||||
return {
|
||||
'contacts': [_contact_row_to_dict(row) for row in rows],
|
||||
}
|
||||
|
||||
|
||||
def get_techstack():
|
||||
"""技术栈 svg 字段:可为 data/logo 下文件名,或 http(s) 外链 SVG 地址。"""
|
||||
with get_connection() as conn:
|
||||
sec = conn.execute('SELECT title FROM techstack_section WHERE id = 1').fetchone()
|
||||
title = sec['title'] if sec else '技术栈'
|
||||
rows = conn.execute(
|
||||
'SELECT id, name, link, svg, color, is_show FROM techstack_item ORDER BY sort_order, id'
|
||||
).fetchall()
|
||||
return {
|
||||
'title': title,
|
||||
'items': [
|
||||
{
|
||||
'id': row['id'],
|
||||
'name': row['name'],
|
||||
'link': row['link'] or '',
|
||||
'svg': row['svg'] or '',
|
||||
'color': row['color'] or '',
|
||||
'show': bool(row['is_show']),
|
||||
}
|
||||
for row in rows
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_all(include_hidden_contacts=False):
|
||||
with get_connection() as conn:
|
||||
profile_row = conn.execute(
|
||||
'''SELECT favicon, nickname, avatar, introduction, footer, site, homepage
|
||||
FROM profile WHERE id = 1'''
|
||||
).fetchone()
|
||||
profile = _row_to_profile(profile_row)
|
||||
|
||||
tech_sec = conn.execute('SELECT title FROM techstack_section WHERE id = 1').fetchone()
|
||||
tech_title = tech_sec['title'] if tech_sec else '技术栈'
|
||||
tech_rows = conn.execute(
|
||||
'SELECT id, name, link, svg, color, is_show FROM techstack_item ORDER BY sort_order, id'
|
||||
).fetchall()
|
||||
techstack = {
|
||||
'title': tech_title,
|
||||
'items': [
|
||||
{
|
||||
'id': r['id'],
|
||||
'name': r['name'],
|
||||
'link': r['link'] or '',
|
||||
'svg': r['svg'] or '',
|
||||
'color': r['color'] or '',
|
||||
'show': bool(r['is_show']),
|
||||
}
|
||||
for r in tech_rows
|
||||
],
|
||||
}
|
||||
|
||||
proj_rows = conn.execute(
|
||||
'SELECT id, title, description, link, icon, is_admin, is_show, is_develop FROM project ORDER BY sort_order, id'
|
||||
).fetchall()
|
||||
projects = []
|
||||
for row in proj_rows:
|
||||
tags = [
|
||||
r['tag']
|
||||
for r in conn.execute(
|
||||
'SELECT tag FROM project_tag WHERE project_id = ? ORDER BY sort_order, id',
|
||||
(row['id'],),
|
||||
)
|
||||
]
|
||||
tech_tags = [
|
||||
r['tag']
|
||||
for r in conn.execute(
|
||||
'SELECT tag FROM project_tech_tag WHERE project_id = ? ORDER BY sort_order, id',
|
||||
(row['id'],),
|
||||
)
|
||||
]
|
||||
projects.append({
|
||||
'id': row['id'],
|
||||
'title': row['title'],
|
||||
'description': row['description'] or '',
|
||||
'link': row['link'] or '',
|
||||
'icon': row['icon'] or '',
|
||||
'tags': tags,
|
||||
'techTags': tech_tags,
|
||||
'admin': bool(row['is_admin']),
|
||||
'show': bool(row['is_show']),
|
||||
'develop': bool(row['is_develop']),
|
||||
})
|
||||
|
||||
contact_rows = conn.execute(
|
||||
'SELECT id, type, label, value, link, icon, is_show FROM contact ORDER BY sort_order, id'
|
||||
).fetchall()
|
||||
contact_list = []
|
||||
for cr in contact_rows:
|
||||
if not include_hidden_contacts and not bool(cr['is_show']):
|
||||
continue
|
||||
contact_list.append(_contact_row_to_dict(cr))
|
||||
contacts = {'contacts': contact_list}
|
||||
|
||||
return {
|
||||
'profile': profile,
|
||||
'techstack': techstack,
|
||||
'projects': {'projects': projects},
|
||||
'contacts': contacts,
|
||||
}
|
||||
|
||||
|
||||
def update_profile(payload):
|
||||
allowed = {'favicon', 'nickname', 'avatar', 'introduction', 'footer', 'site', 'homepage'}
|
||||
updates = {k: payload[k] for k in allowed if k in payload}
|
||||
if not updates:
|
||||
return
|
||||
sets = ', '.join(f'{k} = ?' for k in updates)
|
||||
vals = list(updates.values())
|
||||
with get_connection() as conn:
|
||||
conn.execute(f'UPDATE profile SET {sets} WHERE id = 1', vals)
|
||||
|
||||
|
||||
def create_project(data):
|
||||
with get_connection() as conn:
|
||||
cat_tags, tech_tags = _project_tag_payload_from_body(conn, data)
|
||||
m = conn.execute('SELECT COALESCE(MAX(sort_order), 0) AS m FROM project').fetchone()['m']
|
||||
sort_order = int(m) + 10
|
||||
cur = conn.execute(
|
||||
'''INSERT INTO project (sort_order, title, description, link, icon, is_admin, is_show, is_develop)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||
(
|
||||
sort_order,
|
||||
data['title'],
|
||||
data.get('description') or '',
|
||||
data.get('link') or '',
|
||||
data.get('icon') or '',
|
||||
1 if data.get('admin') else 0,
|
||||
1 if data.get('show', True) else 0,
|
||||
1 if data.get('develop') else 0,
|
||||
),
|
||||
)
|
||||
pid = cur.lastrowid
|
||||
_replace_project_tags(conn, pid, cat_tags)
|
||||
_replace_project_tech_tags(conn, pid, tech_tags)
|
||||
return pid
|
||||
|
||||
|
||||
def update_project(project_id, data):
|
||||
with get_connection() as conn:
|
||||
fields = []
|
||||
vals = []
|
||||
if 'title' in data:
|
||||
fields.append('title = ?')
|
||||
vals.append(data['title'])
|
||||
if 'description' in data:
|
||||
fields.append('description = ?')
|
||||
vals.append(data['description'] or '')
|
||||
if 'link' in data:
|
||||
fields.append('link = ?')
|
||||
vals.append(data['link'] or '')
|
||||
if 'icon' in data:
|
||||
fields.append('icon = ?')
|
||||
vals.append(data['icon'] or '')
|
||||
if 'admin' in data:
|
||||
fields.append('is_admin = ?')
|
||||
vals.append(1 if data['admin'] else 0)
|
||||
if 'show' in data:
|
||||
fields.append('is_show = ?')
|
||||
vals.append(1 if data['show'] else 0)
|
||||
if 'develop' in data:
|
||||
fields.append('is_develop = ?')
|
||||
vals.append(1 if data['develop'] else 0)
|
||||
if fields:
|
||||
vals.append(project_id)
|
||||
conn.execute(
|
||||
f"UPDATE project SET {', '.join(fields)} WHERE id = ?",
|
||||
vals,
|
||||
)
|
||||
if 'tags' in data or 'techTags' in data:
|
||||
if 'tags' in data and 'techTags' in data:
|
||||
_replace_project_tags(conn, project_id, data['tags'])
|
||||
_replace_project_tech_tags(conn, project_id, data['techTags'])
|
||||
elif 'tags' in data:
|
||||
cat, tech = _split_merged_tags(conn, data['tags'])
|
||||
_replace_project_tags(conn, project_id, cat)
|
||||
_replace_project_tech_tags(conn, project_id, tech)
|
||||
else:
|
||||
_replace_project_tech_tags(conn, project_id, data['techTags'])
|
||||
|
||||
|
||||
def delete_project(project_id):
|
||||
with get_connection() as conn:
|
||||
conn.execute('DELETE FROM project WHERE id = ?', (project_id,))
|
||||
|
||||
|
||||
def create_contact(data):
|
||||
with get_connection() as conn:
|
||||
m = conn.execute('SELECT COALESCE(MAX(sort_order), 0) AS m FROM contact').fetchone()['m']
|
||||
sort_order = int(m) + 10
|
||||
cur = conn.execute(
|
||||
'''INSERT INTO contact (sort_order, type, label, value, link, icon, is_show)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)''',
|
||||
(
|
||||
sort_order,
|
||||
data['type'],
|
||||
data.get('label') or '',
|
||||
data.get('value') or '',
|
||||
data.get('link') or '',
|
||||
data.get('icon') or '',
|
||||
1 if data.get('show', True) else 0,
|
||||
),
|
||||
)
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def update_contact(contact_id, data):
|
||||
with get_connection() as conn:
|
||||
fields = []
|
||||
vals = []
|
||||
for col, key in (
|
||||
('type', 'type'),
|
||||
('label', 'label'),
|
||||
('value', 'value'),
|
||||
('link', 'link'),
|
||||
('icon', 'icon'),
|
||||
):
|
||||
if key in data:
|
||||
fields.append(f'{col} = ?')
|
||||
vals.append(data[key] or '')
|
||||
if 'show' in data:
|
||||
fields.append('is_show = ?')
|
||||
vals.append(1 if data['show'] else 0)
|
||||
if fields:
|
||||
vals.append(contact_id)
|
||||
conn.execute(
|
||||
f"UPDATE contact SET {', '.join(fields)} WHERE id = ?",
|
||||
vals,
|
||||
)
|
||||
|
||||
|
||||
def delete_contact(contact_id):
|
||||
with get_connection() as conn:
|
||||
conn.execute('DELETE FROM contact WHERE id = ?', (contact_id,))
|
||||
|
||||
|
||||
def set_techstack_title(title):
|
||||
t = title or '技术栈'
|
||||
with get_connection() as conn:
|
||||
row = conn.execute('SELECT id FROM techstack_section WHERE id = 1').fetchone()
|
||||
if row:
|
||||
conn.execute('UPDATE techstack_section SET title = ? WHERE id = 1', (t,))
|
||||
else:
|
||||
conn.execute('INSERT INTO techstack_section (id, title) VALUES (1, ?)', (t,))
|
||||
|
||||
|
||||
def create_techstack_item(data):
|
||||
with get_connection() as conn:
|
||||
m = conn.execute('SELECT COALESCE(MAX(sort_order), 0) AS m FROM techstack_item').fetchone()['m']
|
||||
sort_order = int(m) + 10
|
||||
cur = conn.execute(
|
||||
'''INSERT INTO techstack_item (sort_order, name, link, svg, color, is_show)
|
||||
VALUES (?, ?, ?, ?, ?, ?)''',
|
||||
(
|
||||
sort_order,
|
||||
data['name'],
|
||||
data.get('link') or '',
|
||||
data.get('svg') or '',
|
||||
data.get('color') or '',
|
||||
1 if data.get('show', True) else 0,
|
||||
),
|
||||
)
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def update_techstack_item(item_id, data):
|
||||
with get_connection() as conn:
|
||||
fields = []
|
||||
vals = []
|
||||
if 'name' in data:
|
||||
fields.append('name = ?')
|
||||
vals.append(data['name'])
|
||||
if 'link' in data:
|
||||
fields.append('link = ?')
|
||||
vals.append(data['link'] or '')
|
||||
if 'svg' in data:
|
||||
fields.append('svg = ?')
|
||||
vals.append(data['svg'] or '')
|
||||
if 'color' in data:
|
||||
fields.append('color = ?')
|
||||
vals.append(data['color'] or '')
|
||||
if 'show' in data:
|
||||
fields.append('is_show = ?')
|
||||
vals.append(1 if data['show'] else 0)
|
||||
if fields:
|
||||
vals.append(item_id)
|
||||
conn.execute(
|
||||
f"UPDATE techstack_item SET {', '.join(fields)} WHERE id = ?",
|
||||
vals,
|
||||
)
|
||||
|
||||
|
||||
def delete_techstack_item(item_id):
|
||||
with get_connection() as conn:
|
||||
conn.execute('DELETE FROM techstack_item WHERE id = ?', (item_id,))
|
||||
10
mengyaprofile-backend/docker-build.bat
Normal file
@@ -0,0 +1,10 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
echo Building mengyaprofile-backend ...
|
||||
docker build -t mengyaprofile-backend:latest .
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo OK: image mengyaprofile-backend:latest
|
||||
echo Run: docker compose up -d
|
||||
endlocal
|
||||
8
mengyaprofile-backend/docker-compose-up.bat
Normal file
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
docker compose up -d --build
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
docker compose ps
|
||||
endlocal
|
||||
@@ -1,19 +1,26 @@
|
||||
version: '3.8'
|
||||
# 后端 API:构建并后台运行
|
||||
# 用法(在 mengyaprofile-backend 目录): docker compose up -d --build
|
||||
# 数据持久化:请把下方卷路径改成你服务器上的目录(Windows 可用 WSL2 或改为命名卷)
|
||||
|
||||
services:
|
||||
mengyaprofile-backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: mengyaprofile-backend:latest
|
||||
container_name: mengyaprofile-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1616:5000"
|
||||
volumes:
|
||||
# 仅持久化 SQLite 与用户上传背景图等;镜像内已带 data/logo
|
||||
- /shumengya/docker/mengyaprofile-backend/data:/app/data
|
||||
environment:
|
||||
- RUN_MODE=production
|
||||
- DATA_DIR=/app/data
|
||||
RUN_MODE: production
|
||||
DATA_DIR: /app/data
|
||||
PORT: "5000"
|
||||
# 生产务必修改:docker compose 可配合 .env 文件设置 ADMIN_TOKEN
|
||||
# ADMIN_TOKEN: your-secret-token
|
||||
networks:
|
||||
- mengyaprofile-network
|
||||
|
||||
|
||||
8
mengyaprofile-backend/docker-entrypoint.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# 宿主机挂载 /app/data 时会遮住镜像内 logo;首次或空目录时从镜像内副本灌入
|
||||
mkdir -p /app/data/logo /app/data/background
|
||||
if [ ! "$(ls -A /app/data/logo 2>/dev/null)" ]; then
|
||||
cp -a /app/logo-dist/. /app/data/logo/
|
||||
fi
|
||||
exec "$@"
|
||||
@@ -1,2 +1,3 @@
|
||||
Flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
gunicorn==21.2.0
|
||||
|
||||
78
mengyaprofile-backend/schema.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- 萌芽主页站点数据(SQLite)
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- 基本个人信息
|
||||
CREATE TABLE IF NOT EXISTS profile (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
favicon TEXT,
|
||||
nickname TEXT NOT NULL DEFAULT '',
|
||||
avatar TEXT,
|
||||
introduction TEXT,
|
||||
footer TEXT,
|
||||
site TEXT,
|
||||
homepage TEXT
|
||||
);
|
||||
|
||||
-- 项目列表
|
||||
CREATE TABLE IF NOT EXISTS project (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
link TEXT,
|
||||
icon TEXT,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0 CHECK (is_admin IN (0, 1)),
|
||||
is_show INTEGER NOT NULL DEFAULT 1 CHECK (is_show IN (0, 1)),
|
||||
is_develop INTEGER NOT NULL DEFAULT 0 CHECK (is_develop IN (0, 1))
|
||||
);
|
||||
|
||||
-- 项目分类 / 业务标签
|
||||
CREATE TABLE IF NOT EXISTS project_tag (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL REFERENCES project (id) ON DELETE CASCADE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
tag TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_tag_project ON project_tag (project_id, sort_order);
|
||||
|
||||
-- 项目技术栈标签(与 project_tag 分开维护)
|
||||
CREATE TABLE IF NOT EXISTS project_tech_tag (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL REFERENCES project (id) ON DELETE CASCADE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
tag TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_tech_tag_project ON project_tech_tag (project_id, sort_order);
|
||||
|
||||
-- 联系方式
|
||||
CREATE TABLE IF NOT EXISTS contact (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
type TEXT NOT NULL,
|
||||
label TEXT,
|
||||
value TEXT,
|
||||
link TEXT,
|
||||
icon TEXT,
|
||||
is_show INTEGER NOT NULL DEFAULT 1 CHECK (is_show IN (0, 1))
|
||||
);
|
||||
|
||||
-- 技术栈分类
|
||||
CREATE TABLE IF NOT EXISTS techstack_section (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
title TEXT NOT NULL DEFAULT '技术栈'
|
||||
);
|
||||
|
||||
-- 技术栈项
|
||||
CREATE TABLE IF NOT EXISTS techstack_item (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
name TEXT NOT NULL,
|
||||
link TEXT,
|
||||
svg TEXT,
|
||||
color TEXT,
|
||||
is_show INTEGER NOT NULL DEFAULT 1 CHECK (is_show IN (0, 1))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_techstack_item_order ON techstack_item (sort_order, id);
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/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()
|
||||
|
||||
42
mengyaprofile-frontend/package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
}
|
||||
@@ -3072,6 +3073,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-babel": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
||||
@@ -13888,6 +13898,38 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.30.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
|
||||
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.30.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
|
||||
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.23.2",
|
||||
"react-router": "6.30.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#52b788" />
|
||||
<meta name="theme-color" content="#e8897a" />
|
||||
<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="萌芽主页" />
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait-primary",
|
||||
"theme_color": "#52b788",
|
||||
"background_color": "#a8e6cf",
|
||||
"theme_color": "#e8897a",
|
||||
"background_color": "#fff5ef",
|
||||
"categories": ["personalization", "productivity"],
|
||||
"prefer_related_applications": false
|
||||
}
|
||||
|
||||
658
mengyaprofile-frontend/src/AdminDashboard.css
Normal file
@@ -0,0 +1,658 @@
|
||||
.admin-page {
|
||||
min-height: 100vh;
|
||||
background: var(--warm-gradient-page);
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.admin-top {
|
||||
max-width: 960px;
|
||||
margin: 0 auto 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.admin-top h1 {
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
color: #5c3d42;
|
||||
}
|
||||
|
||||
.admin-top-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-top-actions button {
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
color: #5c3d42;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.admin-top-actions button.danger {
|
||||
background: #e07a5f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.admin-tabs {
|
||||
max-width: 960px;
|
||||
margin: 0 auto 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-tabs button {
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
color: #5c3d42;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.admin-tabs button.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.admin-panel {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.admin-msg {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: var(--accent-link);
|
||||
}
|
||||
|
||||
.admin-msg.err {
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.admin-form-grid label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.admin-form-grid input,
|
||||
.admin-form-grid textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 复选框/单选框不应被拉成整行宽,否则文字会「飞到」行尾 */
|
||||
.admin-form-grid input[type='checkbox'],
|
||||
.admin-form-grid input[type='radio'] {
|
||||
width: auto;
|
||||
min-width: 1.125rem;
|
||||
max-width: none;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* 覆盖 .admin-form-grid label { display:block },与勾选文字同一行对齐 */
|
||||
.admin-form-grid label.admin-check {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-form-grid textarea {
|
||||
min-height: 72px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.admin-form-actions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.admin-form-actions button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 技术栈「已有项」:多列网格,减少纵向滚动 */
|
||||
.admin-tech-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.admin-tech-card {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
background: #fafafa;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-tech-card .admin-row-head {
|
||||
margin-bottom: 0;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.admin-tech-card__ico {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-tech-card__ph {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.admin-tech-card__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #5c3d42;
|
||||
}
|
||||
|
||||
.admin-tech-card .admin-row-btns {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-row {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.admin-row-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.admin-row-head strong {
|
||||
color: #5c3d42;
|
||||
}
|
||||
|
||||
.admin-row-btns {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-row-btns button {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.admin-row-btns button.warn {
|
||||
background: #fde8e4;
|
||||
border-color: #e07a5f;
|
||||
color: #922b21;
|
||||
}
|
||||
|
||||
.admin-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-right: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-check-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.admin-summary {
|
||||
margin-bottom: 16px;
|
||||
padding: 14px;
|
||||
background: #fff8f5;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #fce0d8;
|
||||
}
|
||||
|
||||
.admin-summary p {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.admin-summary-meta {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.admin-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.admin-toolbar-btn {
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
background: #e8e8e8;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.admin-toolbar-btn.primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.admin-toolbar-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.admin-list-heading {
|
||||
margin: 0 0 12px;
|
||||
font-size: 1rem;
|
||||
color: #5c3d42;
|
||||
}
|
||||
|
||||
.admin-row-sub {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* 管理后台 · 项目列表卡片 */
|
||||
.admin-project-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.admin-project-card__icon-wrap {
|
||||
flex-shrink: 0;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-project-card__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.admin-project-card__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-project-card__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.admin-project-card__title {
|
||||
font-size: 1rem;
|
||||
color: #5c3d42;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.admin-project-card__desc {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
line-height: 1.45;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-project-card__link {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
font-size: 12px;
|
||||
color: var(--accent-link);
|
||||
word-break: break-all;
|
||||
text-decoration: none;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.admin-project-card__link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.admin-project-card__link--empty {
|
||||
color: #999;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.admin-project-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-project-card__tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #fff0eb;
|
||||
color: #9d4e56;
|
||||
border: 1px solid #f5cfc4;
|
||||
}
|
||||
|
||||
.admin-project-card__tag--more {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.admin-project-card__tag--tech {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
border-color: #90caf9;
|
||||
}
|
||||
|
||||
.admin-project-card__pill {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-project-card__pill--dev {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
border: 1px solid #90caf9;
|
||||
}
|
||||
|
||||
.admin-project-card__pill--deploy {
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
border: 1px solid #ffcc80;
|
||||
}
|
||||
|
||||
.admin-project-card__pill--admin {
|
||||
background: #fce4ec;
|
||||
color: #c2185b;
|
||||
border: 1px solid #f48fb1;
|
||||
}
|
||||
|
||||
.admin-project-card__pill--hidden {
|
||||
background: #eceff1;
|
||||
color: #546e7a;
|
||||
border: 1px solid #b0bec5;
|
||||
}
|
||||
|
||||
.admin-inline-title {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.admin-title-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-title-row input {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.admin-tech-picker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* 项目弹窗加宽时:技术栈格子与图标放大,便于点击 */
|
||||
.admin-modal-panel--wide .admin-tech-picker {
|
||||
gap: 12px;
|
||||
max-height: min(52vh, 480px);
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.admin-modal-panel--wide .admin-tech-pick {
|
||||
width: 104px;
|
||||
min-width: 104px;
|
||||
min-height: 96px;
|
||||
padding: 12px 8px;
|
||||
gap: 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-modal-panel--wide .admin-tech-pick__img {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.admin-modal-panel--wide .admin-tech-pick__ph {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.admin-modal-panel--wide .admin-tech-pick__name {
|
||||
font-size: 12px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.admin-tech-picker-empty {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.admin-tech-pick {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 76px;
|
||||
padding: 8px 6px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
color: #333;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.admin-tech-pick:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.admin-tech-pick--on {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-alpha-14);
|
||||
box-shadow: 0 0 0 1px var(--accent-alpha-35);
|
||||
}
|
||||
|
||||
.admin-tech-pick__img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.admin-tech-pick__ph {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #eee;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.admin-tech-pick__name {
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.admin-project-card__tech-ico {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 3px;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.admin-project-card__tech-ico img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 技术栈项「主题色」:取色器 + 文本 + 快捷色块 */
|
||||
.admin-color-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.admin-form-grid .admin-color-row input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 取色器宽度需压过 .admin-form-grid input { width:100% } */
|
||||
.admin-form-grid .admin-color-row input[type='color'] {
|
||||
width: 52px;
|
||||
min-width: 52px;
|
||||
max-width: 52px;
|
||||
flex-shrink: 0;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.admin-color-native {
|
||||
width: 52px;
|
||||
min-width: 52px;
|
||||
height: 42px;
|
||||
padding: 3px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.admin-color-native::-webkit-color-swatch-wrapper {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.admin-color-native::-webkit-color-swatch {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.admin-color-hex-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-color-swatches {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-color-swatch {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
.admin-color-swatch:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.admin-color-swatch--light {
|
||||
border-color: #bbb;
|
||||
}
|
||||
1053
mengyaprofile-frontend/src/AdminDashboard.js
Normal file
@@ -17,9 +17,8 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-family: 'Inter', 'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
|
||||
'Segoe UI', system-ui, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow-y: scroll;
|
||||
@@ -28,21 +27,21 @@ body {
|
||||
|
||||
.App {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3b6 100%);
|
||||
background: var(--warm-gradient-page);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 背景图片高斯模糊遮罩层 */
|
||||
.background-overlay {
|
||||
/* 全站随机背景(约 10% 强度:blur 10px) */
|
||||
.app-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
backdrop-filter: blur(3px);
|
||||
-webkit-backdrop-filter: blur(3px);
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: blur(10px);
|
||||
-webkit-filter: blur(10px);
|
||||
transform: scale(1.06);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -54,9 +53,10 @@ body {
|
||||
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%);
|
||||
z-index: 1;
|
||||
background:
|
||||
radial-gradient(circle at 20% 45%, rgba(255, 236, 228, 0.55) 0%, transparent 52%),
|
||||
radial-gradient(circle at 82% 78%, rgba(255, 218, 228, 0.45) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
animation: bgFloat 20s ease-in-out infinite;
|
||||
}
|
||||
@@ -71,7 +71,7 @@ body {
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
@@ -116,7 +116,7 @@ body {
|
||||
.pwa-launch-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 40%, #ffd3b6 100%);
|
||||
background: var(--warm-gradient-page-pwa);
|
||||
animation: pwaLaunchBgPulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ body {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border: 2px solid rgba(82, 183, 136, 0.4);
|
||||
border: 2px solid var(--accent-alpha-40);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
@@ -236,7 +236,7 @@ body {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #52b788;
|
||||
background: var(--accent);
|
||||
animation: pwaLaunchDot 1.2s ease-in-out infinite both;
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ body {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.footer-visitor {
|
||||
@@ -303,10 +303,82 @@ body {
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.admin-token-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.admin-token-box {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.admin-token-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.admin-token-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.admin-token-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.admin-token-err {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.admin-token-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.admin-token-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.admin-token-btn.secondary {
|
||||
background: #eee;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.admin-token-btn.primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 20px 15px;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.error-container h2 {
|
||||
|
||||
@@ -1,244 +1,13 @@
|
||||
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);
|
||||
const [visitorInfo, setVisitorInfo] = useState(null);
|
||||
const [visitorInfoLoading, setVisitorInfoLoading] = useState(true);
|
||||
|
||||
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获取所有数据
|
||||
// 使用环境变量配置的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');
|
||||
fetch(`${apiBaseUrl}/all`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('网络响应失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
setData(data);
|
||||
|
||||
// 设置 favicon:优先使用 cf-favicon API,失败则用后端返回的 logo
|
||||
const applyFavicon = (url) => {
|
||||
let faviconTag = document.querySelector('link[rel="icon"]');
|
||||
if (!faviconTag) {
|
||||
faviconTag = document.createElement('link');
|
||||
faviconTag.rel = 'icon';
|
||||
document.head.appendChild(faviconTag);
|
||||
}
|
||||
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) {
|
||||
fetch(`${apiBaseUrl}/random-background`)
|
||||
.then(res => res.json())
|
||||
.then(bgData => {
|
||||
if (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));
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取数据失败:', error);
|
||||
setError(error.message);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div className="pwa-launch">
|
||||
<div className="pwa-launch-bg" />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="error-container">
|
||||
<h2>加载失败</h2>
|
||||
<p>{error}</p>
|
||||
<p className="error-hint">请确保后端服务已启动(运行 python app.py)</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import HomePage from './HomePage';
|
||||
import AdminDashboard from './AdminDashboard';
|
||||
|
||||
export default function App() {
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/admin" element={<AdminDashboard />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
|
||||
const mockAllData = {
|
||||
@@ -6,7 +7,6 @@ const mockAllData = {
|
||||
nickname: 'Test User',
|
||||
avatar: null,
|
||||
introduction: 'Hello',
|
||||
showlocalbackground: false,
|
||||
footer: '© Test Footer'
|
||||
},
|
||||
techstack: null,
|
||||
@@ -17,7 +17,14 @@ const mockAllData = {
|
||||
const mockVisitorInfo = {
|
||||
ip: '66.90.99.202',
|
||||
ipVersion: 'ipv4',
|
||||
geo: { country: 'JP', region: 'Tokyo', city: 'Ebara' }
|
||||
geo: {
|
||||
country: 'JP',
|
||||
countryName: '日本',
|
||||
region: 'Tokyo',
|
||||
regionName: '东京都',
|
||||
city: 'Ebara',
|
||||
cityName: '东京'
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -34,6 +41,12 @@ beforeEach(() => {
|
||||
json: () => Promise.resolve(mockVisitorInfo)
|
||||
});
|
||||
}
|
||||
if (typeof url === 'string' && url.includes('randbg.api.smyhub.com')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ url: 'https://example.com/bg.webp' })
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({})
|
||||
@@ -46,8 +59,13 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
test('renders visitor ip and geo in footer', async () => {
|
||||
render(<App />);
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<App />
|
||||
</MemoryRouter>
|
||||
);
|
||||
const visitorLine = await screen.findByText(/访客 IP:66\.90\.99\.202/i);
|
||||
expect(visitorLine).toBeInTheDocument();
|
||||
expect(visitorLine).toHaveTextContent('JP');
|
||||
expect(visitorLine).toHaveTextContent('日本');
|
||||
expect(visitorLine).toHaveTextContent('东京都');
|
||||
});
|
||||
|
||||
290
mengyaprofile-frontend/src/HomePage.js
Normal file
@@ -0,0 +1,290 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
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';
|
||||
import { ADMIN_TOKEN_KEY, adminFetch, adminPing, getApiBase, getFaviconUrlForSite } from './api';
|
||||
|
||||
const RANDOM_BG_JSON = 'https://randbg.api.smyhub.com/api/random?format=json';
|
||||
const AVATAR_TAP_RESET_MS = 2000;
|
||||
const AVATAR_TAPS_REQUIRED = 5;
|
||||
|
||||
function formatVisitorGeoLine(geo) {
|
||||
if (!geo) return null;
|
||||
const parts = [
|
||||
geo.countryName || geo.country,
|
||||
geo.regionName || geo.region,
|
||||
geo.cityName || geo.city
|
||||
].filter(Boolean);
|
||||
return parts.length ? parts.join(' · ') : null;
|
||||
}
|
||||
|
||||
function VisitorIpLine({ ip, geo }) {
|
||||
const geoLine = formatVisitorGeoLine(geo);
|
||||
return (
|
||||
<span>
|
||||
访客 IP:{ip}
|
||||
{geoLine ? <> · {geoLine}</> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const navigate = useNavigate();
|
||||
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 [visitorInfo, setVisitorInfo] = useState(null);
|
||||
const [visitorInfoLoading, setVisitorInfoLoading] = useState(true);
|
||||
const [tokenModalOpen, setTokenModalOpen] = useState(false);
|
||||
const [tokenInput, setTokenInput] = useState('');
|
||||
const [tokenError, setTokenError] = useState('');
|
||||
const avatarTapRef = useRef({ count: 0, lastAt: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const token = sessionStorage.getItem(ADMIN_TOKEN_KEY);
|
||||
if (!token) return;
|
||||
adminFetch('/admin/ping').then((r) => {
|
||||
if (!r.ok) sessionStorage.removeItem(ADMIN_TOKEN_KEY);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const apiBaseUrl = getApiBase();
|
||||
fetch(`${apiBaseUrl}/all`)
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error('网络响应失败');
|
||||
return response.json();
|
||||
})
|
||||
.then((d) => {
|
||||
setData(d);
|
||||
const applyFavicon = (url) => {
|
||||
let faviconTag = document.querySelector('link[rel="icon"]');
|
||||
if (!faviconTag) {
|
||||
faviconTag = document.createElement('link');
|
||||
faviconTag.rel = 'icon';
|
||||
document.head.appendChild(faviconTag);
|
||||
}
|
||||
faviconTag.href = url;
|
||||
};
|
||||
const fallbackFavicon = d.profile?.favicon || '';
|
||||
const siteUrl =
|
||||
d.profile?.site ||
|
||||
d.profile?.homepage ||
|
||||
d.contacts?.contacts?.find((c) => c.type === 'personprofile')?.link ||
|
||||
d.contacts?.contacts?.find((c) => c.link?.startsWith('https://'))?.link ||
|
||||
window.location.origin;
|
||||
const apiFaviconUrl = getFaviconUrlForSite(siteUrl);
|
||||
const img = new Image();
|
||||
img.onload = () => applyFavicon(apiFaviconUrl);
|
||||
img.onerror = () => {
|
||||
if (fallbackFavicon) applyFavicon(fallbackFavicon);
|
||||
};
|
||||
img.src = apiFaviconUrl;
|
||||
if (fallbackFavicon && !siteUrl) applyFavicon(fallbackFavicon);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('获取数据失败:', err);
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const ac = new AbortController();
|
||||
fetch(RANDOM_BG_JSON, { signal: ac.signal })
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json();
|
||||
})
|
||||
.then((j) => {
|
||||
if (j?.url) setBackgroundImage(j.url);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err?.name === 'AbortError') return;
|
||||
console.warn('随机背景加载失败:', err);
|
||||
});
|
||||
return () => ac.abort();
|
||||
}, []);
|
||||
|
||||
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();
|
||||
}, []);
|
||||
|
||||
const onAvatarAdminTap = useCallback(() => {
|
||||
const now = Date.now();
|
||||
if (now - avatarTapRef.current.lastAt > AVATAR_TAP_RESET_MS) {
|
||||
avatarTapRef.current.count = 0;
|
||||
}
|
||||
avatarTapRef.current.lastAt = now;
|
||||
avatarTapRef.current.count += 1;
|
||||
if (avatarTapRef.current.count >= AVATAR_TAPS_REQUIRED) {
|
||||
avatarTapRef.current.count = 0;
|
||||
setTokenInput('');
|
||||
setTokenError('');
|
||||
setTokenModalOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const submitAdminToken = async () => {
|
||||
const t = tokenInput.trim();
|
||||
if (!t) {
|
||||
setTokenError('请输入 token');
|
||||
return;
|
||||
}
|
||||
const ok = await adminPing(t);
|
||||
if (!ok) {
|
||||
setTokenError('token 无效');
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem(ADMIN_TOKEN_KEY, t);
|
||||
setTokenModalOpen(false);
|
||||
navigate('/admin');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pwa-launch">
|
||||
<div className="pwa-launch-bg" />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
{backgroundImage ? (
|
||||
<div
|
||||
className="app-background"
|
||||
style={{ backgroundImage: `url(${backgroundImage})` }}
|
||||
aria-hidden
|
||||
/>
|
||||
) : null}
|
||||
<ClickParticle />
|
||||
|
||||
<div className="container">
|
||||
{data.profile && (
|
||||
<ProfileSection profile={data.profile} onAvatarAdminTap={onAvatarAdminTap} />
|
||||
)}
|
||||
{data.techstack && <TechStackSection techstack={data.techstack} />}
|
||||
{data.projects && (
|
||||
<ProjectsSection
|
||||
projects={data.projects.projects}
|
||||
techStackItems={data.techstack?.items || []}
|
||||
/>
|
||||
)}
|
||||
{data.contacts && <ContactsSection contacts={data.contacts.contacts} />}
|
||||
</div>
|
||||
|
||||
<footer className="footer">
|
||||
<p>
|
||||
<strong>
|
||||
{data.profile?.footer || '© 2025 萌芽个人主页. All rights reserved.'}
|
||||
</strong>
|
||||
</p>
|
||||
<div className="footer-visitor">
|
||||
{visitorInfoLoading ? (
|
||||
<span>访客信息加载中</span>
|
||||
) : visitorInfo?.ip ? (
|
||||
<VisitorIpLine ip={visitorInfo.ip} geo={visitorInfo.geo} />
|
||||
) : (
|
||||
<span>访客信息获取失败</span>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{tokenModalOpen ? (
|
||||
<div
|
||||
className="admin-token-backdrop"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="管理入口"
|
||||
onMouseDown={(e) => {
|
||||
if (e.target === e.currentTarget) setTokenModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="admin-token-box">
|
||||
<p className="admin-token-title">管理验证</p>
|
||||
<input
|
||||
type="password"
|
||||
className="admin-token-input"
|
||||
placeholder="Token"
|
||||
value={tokenInput}
|
||||
onChange={(e) => setTokenInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && submitAdminToken()}
|
||||
autoFocus
|
||||
/>
|
||||
{tokenError ? <p className="admin-token-err">{tokenError}</p> : null}
|
||||
<div className="admin-token-actions">
|
||||
<button type="button" className="admin-token-btn secondary" onClick={() => setTokenModalOpen(false)}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" className="admin-token-btn primary" onClick={submitAdminToken}>
|
||||
进入
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
mengyaprofile-frontend/src/api.js
Normal file
@@ -0,0 +1,61 @@
|
||||
export const ADMIN_TOKEN_KEY = 'mengya_admin_token';
|
||||
|
||||
/** 站点 logo / favicon 解析(固定) */
|
||||
export const FAVICON_API_URL = 'https://favicon.smyhub.com/api/favicon?url=';
|
||||
|
||||
export function getFaviconUrlForSite(siteUrl) {
|
||||
return `${FAVICON_API_URL}${encodeURIComponent(siteUrl)}`;
|
||||
}
|
||||
|
||||
export function getApiBase() {
|
||||
return (
|
||||
process.env.REACT_APP_API_URL ||
|
||||
(process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:5000/api'
|
||||
: 'https://nav.api.shumengya.top/api')
|
||||
);
|
||||
}
|
||||
|
||||
/** 技术栈项 svg:外链 URL 或 data/logo 下文件名 */
|
||||
export function getTechstackIconUrl(item) {
|
||||
const s = (item?.svg || '').trim();
|
||||
if (!s) return null;
|
||||
if (/^https?:\/\//i.test(s)) return s;
|
||||
if (s.startsWith('//')) return `https:${s}`;
|
||||
return `${getApiBase()}/logo/${s}`;
|
||||
}
|
||||
|
||||
/** 技术栈徽章底色:支持 #hex 或 CSS linear-gradient(...) */
|
||||
export function techBadgeSurfaceStyle(color) {
|
||||
const c = String(color || '#555555').trim();
|
||||
if (c.includes('gradient(')) {
|
||||
return { background: c };
|
||||
}
|
||||
return { backgroundColor: c };
|
||||
}
|
||||
|
||||
/** 与 techBadgeSurfaceStyle 配套的文字颜色 */
|
||||
export function techBadgeLabelColor(color) {
|
||||
const c = String(color || '').trim();
|
||||
if (c.includes('gradient(')) return '#ffffff';
|
||||
const u = c.toUpperCase();
|
||||
if (u === '#FFFFFF' || u === '#FFF') return '#000000';
|
||||
return '#ffffff';
|
||||
}
|
||||
|
||||
export function adminFetch(path, options = {}) {
|
||||
const token = sessionStorage.getItem(ADMIN_TOKEN_KEY);
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {}),
|
||||
'X-Admin-Token': token || '',
|
||||
};
|
||||
return fetch(`${getApiBase()}${path}`, { ...options, headers });
|
||||
}
|
||||
|
||||
export async function adminPing(token) {
|
||||
const res = await fetch(`${getApiBase()}/admin/ping`, {
|
||||
headers: { 'X-Admin-Token': token },
|
||||
});
|
||||
return res.ok;
|
||||
}
|
||||
@@ -5,11 +5,11 @@ 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'
|
||||
'#ffd8cc', '#fbc4ab', '#f5c4d0', '#f4a574', '#e8897a',
|
||||
'#ffb4a2', '#fce8e4', '#fdbcb4', '#ff9a8b',
|
||||
'#ff6b9d', '#c44569', '#ee5a6f', '#fc5c65',
|
||||
'#f8b500', '#ffd93d', '#fed330', '#fa8231',
|
||||
'#fd79a8', '#f472b6', '#e07a5f', '#f59e0b',
|
||||
];
|
||||
|
||||
// 更密集的均匀辐射(一次点击,360°均匀分布)
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
margin-bottom: 40px;
|
||||
animation: fadeInUp 0.8s ease-out 0.4s both;
|
||||
padding: 24px;
|
||||
border: 2px solid rgba(82, 183, 136, 0.3);
|
||||
border: 2px solid var(--accent-alpha-30);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 4px 20px rgba(82, 183, 136, 0.1);
|
||||
box-shadow: 0 4px 20px var(--accent-alpha-10);
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
@@ -40,20 +40,40 @@
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(255, 242, 236, 0.52) 0%,
|
||||
rgba(255, 255, 255, 0.28) 48%,
|
||||
rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.12) 100%
|
||||
);
|
||||
border: 1px solid rgba(245, 190, 175, 0.55);
|
||||
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;
|
||||
box-shadow:
|
||||
0 4px 24px var(--warm-deep-alpha-08),
|
||||
0 1px 0 rgba(255, 232, 226, 0.62) inset;
|
||||
backdrop-filter: blur(14px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.2);
|
||||
transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contact-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(255, 228, 232, 0.62) 0%,
|
||||
rgba(255, 255, 255, 0.32) 48%,
|
||||
rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.2) 100%
|
||||
);
|
||||
border-color: rgba(232, 170, 158, 0.72);
|
||||
box-shadow:
|
||||
0 14px 36px var(--warm-deep-alpha-14),
|
||||
0 1px 0 rgba(255, 236, 240, 0.68) inset;
|
||||
}
|
||||
|
||||
.contact-icon {
|
||||
@@ -63,10 +83,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #52b788, #95d5b2);
|
||||
background: rgba(255, 236, 232, 0.55);
|
||||
border: 1px solid rgba(var(--accent-2-r), var(--accent-2-g), var(--accent-2-b), 0.42);
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 12px rgba(82, 183, 136, 0.3);
|
||||
box-shadow:
|
||||
0 3px 14px var(--warm-deep-alpha-08),
|
||||
0 1px 0 rgba(255, 255, 255, 0.55) inset;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -93,16 +116,18 @@
|
||||
.contact-label {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
color: #1a202c;
|
||||
margin-bottom: 4px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.contact-value {
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
color: #2d3748;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.contact-actions {
|
||||
@@ -115,9 +140,9 @@
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: rgba(82, 183, 136, 0.1);
|
||||
color: #52b788;
|
||||
border: 1px solid rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.28);
|
||||
background: rgba(255, 238, 232, 0.42);
|
||||
color: var(--accent-link);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
@@ -128,7 +153,12 @@
|
||||
}
|
||||
|
||||
.contact-button:hover {
|
||||
background: linear-gradient(135deg, #52b788, #95d5b2);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.88),
|
||||
rgba(var(--accent-2-r), var(--accent-2-g), var(--accent-2-b), 0.82)
|
||||
);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
@@ -141,13 +171,15 @@
|
||||
position: absolute;
|
||||
top: -36px;
|
||||
right: 20px;
|
||||
background: #48bb78;
|
||||
background: rgba(184, 77, 95, 0.9);
|
||||
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);
|
||||
box-shadow: 0 4px 16px var(--warm-deep-alpha-32);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
animation: toastSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@@ -168,51 +200,51 @@
|
||||
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%);
|
||||
|
||||
@@ -4,6 +4,8 @@ import './ContactsSection.css';
|
||||
function ContactsSection({ contacts }) {
|
||||
const [copiedType, setCopiedType] = useState(null);
|
||||
|
||||
const visibleContacts = (contacts || []).filter((c) => c.show !== false);
|
||||
|
||||
const handleCopy = (value, type) => {
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setCopiedType(type);
|
||||
@@ -33,22 +35,32 @@ function ContactsSection({ contacts }) {
|
||||
const icon = contact.icon || getContactIcon(contact.type);
|
||||
|
||||
if (isImageUrl(icon)) {
|
||||
const fallback = getContactIcon(contact.type);
|
||||
return (
|
||||
<img
|
||||
src={icon}
|
||||
alt={contact.label}
|
||||
className="contact-icon-img"
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
e.target.nextSibling.style.display = 'block';
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<img
|
||||
src={icon}
|
||||
alt={contact.label}
|
||||
className="contact-icon-img"
|
||||
onError={(e) => {
|
||||
const img = e.currentTarget;
|
||||
img.style.display = 'none';
|
||||
const el = img.nextElementSibling;
|
||||
if (el) el.style.display = 'block';
|
||||
}}
|
||||
/>
|
||||
<span className="contact-icon-emoji" style={{ display: 'none' }} aria-hidden>
|
||||
{fallback}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className="contact-icon-emoji">{icon}</span>;
|
||||
};
|
||||
|
||||
if (!visibleContacts.length) return null;
|
||||
|
||||
return (
|
||||
<section className="contacts-section">
|
||||
<h2 className="section-title">
|
||||
@@ -57,8 +69,8 @@ function ContactsSection({ contacts }) {
|
||||
</h2>
|
||||
|
||||
<div className="contacts-grid">
|
||||
{contacts.map((contact, index) => (
|
||||
<div key={index} className="contact-card">
|
||||
{visibleContacts.map((contact, index) => (
|
||||
<div key={contact.id != null ? contact.id : index} className="contact-card">
|
||||
<div className="contact-icon">
|
||||
{renderIcon(contact)}
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
animation: avatarPulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@
|
||||
right: -10px;
|
||||
bottom: -10px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #52b788, #95d5b2);
|
||||
background: var(--accent-gradient);
|
||||
opacity: 0.3;
|
||||
animation: ringPulse 3s ease-in-out infinite;
|
||||
}
|
||||
@@ -69,7 +70,7 @@
|
||||
font-weight: 700;
|
||||
color: #2d3748;
|
||||
margin-bottom: 12px;
|
||||
background: linear-gradient(135deg, #52b788, #95d5b2);
|
||||
background: var(--accent-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@@ -82,14 +83,14 @@
|
||||
|
||||
.position-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #52b788, #95d5b2);
|
||||
background: var(--accent-gradient);
|
||||
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);
|
||||
box-shadow: 0 4px 15px var(--accent-alpha-40);
|
||||
}
|
||||
|
||||
.profile-introduction {
|
||||
@@ -110,7 +111,7 @@
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(135deg, rgba(82, 183, 136, 0.1), rgba(149, 213, 178, 0.1));
|
||||
background: linear-gradient(135deg, var(--accent-alpha-10), var(--accent-2-alpha-10));
|
||||
border-radius: 12px;
|
||||
margin-top: 24px;
|
||||
max-width: 500px;
|
||||
@@ -124,7 +125,7 @@
|
||||
|
||||
.motto-text {
|
||||
font-size: 16px;
|
||||
color: #2d6a4f;
|
||||
color: var(--accent-link);
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import './ProfileSection.css';
|
||||
|
||||
function ProfileSection({ profile }) {
|
||||
function ProfileSection({ profile, onAvatarAdminTap }) {
|
||||
return (
|
||||
<section className="profile-section">
|
||||
<div className="profile-card">
|
||||
<div className="profile-avatar-container">
|
||||
<img
|
||||
src={profile.avatar}
|
||||
<img
|
||||
src={profile.avatar}
|
||||
alt={profile.nickname}
|
||||
className="profile-avatar"
|
||||
onClick={() => onAvatarAdminTap?.()}
|
||||
/>
|
||||
<div className="avatar-ring"></div>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
margin-bottom: 40px;
|
||||
animation: fadeInUp 0.8s ease-out 0.2s both;
|
||||
padding: 24px;
|
||||
border: 2px solid rgba(82, 183, 136, 0.3);
|
||||
border: 2px solid var(--accent-alpha-30);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 4px 20px rgba(82, 183, 136, 0.1);
|
||||
box-shadow: 0 4px 20px var(--accent-alpha-10);
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
@@ -63,8 +63,8 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 18px;
|
||||
border: 2px solid rgba(82, 183, 136, 0.3);
|
||||
border-radius: 20px;
|
||||
border: 2px solid var(--accent-alpha-30);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
@@ -76,58 +76,59 @@
|
||||
}
|
||||
|
||||
.category-btn:hover {
|
||||
background: rgba(82, 183, 136, 0.2);
|
||||
border-color: rgba(82, 183, 136, 0.5);
|
||||
background: var(--accent-alpha-20);
|
||||
border-color: var(--accent-alpha-50);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(82, 183, 136, 0.3);
|
||||
box-shadow: 0 4px 12px var(--accent-alpha-30);
|
||||
}
|
||||
|
||||
.category-btn.active {
|
||||
background: linear-gradient(135deg, #52b788, #95d5b2);
|
||||
border-color: #52b788;
|
||||
background: var(--accent-gradient);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(82, 183, 136, 0.4);
|
||||
box-shadow: 0 4px 16px var(--accent-alpha-40);
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-auto-rows: auto;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-auto-rows: 1fr;
|
||||
align-items: stretch;
|
||||
justify-items: stretch;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
/* 当前页所有卡片跟随最大 Grid 行高,填满单元格。 */
|
||||
height: 100%;
|
||||
align-self: stretch;
|
||||
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;
|
||||
transition: background 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
|
||||
border-radius: 14px;
|
||||
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));
|
||||
isolation: isolate;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(255, 242, 236, 0.44) 0%,
|
||||
rgba(255, 255, 255, 0.22) 52%,
|
||||
rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.08) 100%
|
||||
);
|
||||
border: 1px solid rgba(255, 255, 255, 0.55);
|
||||
box-shadow:
|
||||
0 4px 24px rgba(0, 0, 0, 0.07),
|
||||
0 1px 0 rgba(255, 255, 255, 0.65) inset;
|
||||
backdrop-filter: blur(14px) saturate(1.12);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(1.12);
|
||||
}
|
||||
|
||||
.project-card::before {
|
||||
@@ -137,9 +138,11 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #52b788, #95d5b2);
|
||||
background: var(--accent-gradient-90);
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.project-card:hover::before {
|
||||
@@ -148,14 +151,26 @@
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(255, 228, 232, 0.52) 0%,
|
||||
rgba(255, 255, 255, 0.32) 52%,
|
||||
rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.14) 100%
|
||||
);
|
||||
border-color: rgba(255, 230, 235, 0.82);
|
||||
box-shadow:
|
||||
0 14px 36px var(--warm-deep-alpha-12),
|
||||
0 1px 0 rgba(255, 248, 246, 0.78) inset;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-icon {
|
||||
@@ -163,8 +178,9 @@
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08), 0 1px 0 rgba(255, 255, 255, 0.4) inset;
|
||||
flex-shrink: 0;
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.project-icon img {
|
||||
@@ -176,34 +192,89 @@
|
||||
.project-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #2d3748;
|
||||
color: #1a202c;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.project-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #4a5568;
|
||||
color: #2d3748;
|
||||
margin-bottom: 12px;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.project-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 27px;
|
||||
min-height: 27px;
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: 4px 11px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
/* 业务/分类标签:背景/文字/边框由 ProjectsSection 内联按标签名哈希着色 */
|
||||
.project-tag--category {
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
/* 与技术栈板块中的名称对齐的条目(无图标配置时的回退) */
|
||||
.project-tag--tech {
|
||||
background: linear-gradient(135deg, rgba(66, 153, 225, 0.22), rgba(129, 178, 245, 0.12));
|
||||
color: #2c5282;
|
||||
border-color: rgba(66, 153, 225, 0.4);
|
||||
}
|
||||
|
||||
/* 与 TechStackSection 中 .badge-icon img 一致:36×36 */
|
||||
.project-tech-icons {
|
||||
display: flex;
|
||||
flex-wrap: wrap-reverse;
|
||||
align-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 76px;
|
||||
min-height: 76px;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-tech-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-tech-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.project-link-indicator {
|
||||
@@ -211,10 +282,11 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
color: #52b788;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.4);
|
||||
color: var(--accent-link);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
@@ -228,9 +300,13 @@
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.projects-section {
|
||||
padding: 16px 10px;
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
@@ -256,17 +332,7 @@
|
||||
}
|
||||
|
||||
.project-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.develop-badge {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.develop-badge svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 14px 11px;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
@@ -288,27 +354,24 @@
|
||||
}
|
||||
|
||||
.project-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
padding: 3px 9px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 桌面端固定为5列 */
|
||||
@media (min-width: 1441px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) and (max-width: 1440px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
/* 移动端略小于桌面,仍接近上方技术栈徽章的可读尺寸 */
|
||||
.project-tech-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,13 +403,13 @@
|
||||
}
|
||||
|
||||
.pagination-dot.active {
|
||||
background: #52b788;
|
||||
background: var(--accent);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
box-shadow: 0 0 10px rgba(82, 183, 136, 0.5);
|
||||
box-shadow: 0 0 10px var(--accent-alpha-50);
|
||||
}
|
||||
|
||||
.pagination-dot:focus {
|
||||
outline: 2px solid rgba(82, 183, 136, 0.5);
|
||||
outline: 2px solid var(--accent-alpha-50);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,86 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
ADMIN_TOKEN_KEY,
|
||||
getFaviconUrlForSite,
|
||||
getTechstackIconUrl,
|
||||
techBadgeSurfaceStyle,
|
||||
} from '../api';
|
||||
import './ProjectsSection.css';
|
||||
|
||||
function ProjectsSection({ projects }) {
|
||||
function normTag(s) {
|
||||
return String(s || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
/** 分类标签:按文案稳定哈希选色,同一标签全局颜色一致 */
|
||||
const CATEGORY_TAG_PALETTE = [
|
||||
{ background: 'rgba(255, 236, 230, 0.95)', color: '#6b3d38', borderColor: 'rgba(232, 137, 122, 0.45)' },
|
||||
{ background: 'rgba(219, 234, 254, 0.95)', color: '#1e3a8a', borderColor: 'rgba(59, 130, 246, 0.4)' },
|
||||
{ background: 'rgba(254, 243, 199, 0.95)', color: '#78350f', borderColor: 'rgba(245, 158, 11, 0.45)' },
|
||||
{ background: 'rgba(243, 232, 255, 0.95)', color: '#581c87', borderColor: 'rgba(168, 85, 247, 0.4)' },
|
||||
{ background: 'rgba(255, 228, 230, 0.95)', color: '#881337', borderColor: 'rgba(244, 63, 94, 0.4)' },
|
||||
{ background: 'rgba(224, 242, 254, 0.95)', color: '#0c4a6e', borderColor: 'rgba(14, 165, 233, 0.4)' },
|
||||
{ background: 'rgba(237, 233, 254, 0.95)', color: '#4c1d95', borderColor: 'rgba(139, 92, 246, 0.4)' },
|
||||
{ background: 'rgba(254, 249, 195, 0.95)', color: '#713f12', borderColor: 'rgba(234, 179, 8, 0.45)' },
|
||||
{ background: 'rgba(209, 250, 229, 0.95)', color: '#064e3b', borderColor: 'rgba(16, 185, 129, 0.4)' },
|
||||
{ background: 'rgba(255, 237, 213, 0.95)', color: '#7c2d12', borderColor: 'rgba(249, 115, 22, 0.4)' },
|
||||
{ background: 'rgba(224, 231, 255, 0.95)', color: '#312e81', borderColor: 'rgba(99, 102, 241, 0.4)' },
|
||||
{ background: 'rgba(252, 231, 243, 0.95)', color: '#831843', borderColor: 'rgba(236, 72, 153, 0.4)' },
|
||||
{ background: 'rgba(204, 251, 241, 0.95)', color: '#134e4a', borderColor: 'rgba(20, 184, 166, 0.4)' },
|
||||
{ background: 'rgba(254, 240, 138, 0.75)', color: '#422006', borderColor: 'rgba(202, 138, 4, 0.45)' },
|
||||
{ background: 'rgba(233, 213, 255, 0.95)', color: '#4a044e', borderColor: 'rgba(192, 38, 211, 0.35)' },
|
||||
];
|
||||
|
||||
function hashTagString(s) {
|
||||
const str = String(s);
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i += 1) {
|
||||
h ^= str.charCodeAt(i);
|
||||
h = Math.imul(h, 16777619);
|
||||
}
|
||||
return Math.abs(h);
|
||||
}
|
||||
|
||||
function categoryTagStyle(tag) {
|
||||
const idx = hashTagString(tag) % CATEGORY_TAG_PALETTE.length;
|
||||
return CATEGORY_TAG_PALETTE[idx];
|
||||
}
|
||||
|
||||
function ProjectsSection({ projects, techStackItems = [] }) {
|
||||
const location = useLocation();
|
||||
|
||||
const techByNormName = useMemo(() => {
|
||||
const m = new Map();
|
||||
for (const item of techStackItems) {
|
||||
const k = normTag(item.name);
|
||||
if (k) m.set(k, item);
|
||||
}
|
||||
return m;
|
||||
}, [techStackItems]);
|
||||
|
||||
const techNameSet = useMemo(() => new Set(techByNormName.keys()), [techByNormName]);
|
||||
|
||||
const tagIsTech = (tag) => techNameSet.has(normTag(tag));
|
||||
|
||||
const resolveTechItem = (name) => techByNormName.get(normTag(name));
|
||||
|
||||
const [hoveredIndex, setHoveredIndex] = useState(null);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [isAdmin, setIsAdmin] = useState(() => !!sessionStorage.getItem(ADMIN_TOKEN_KEY));
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [category, setCategory] = useState('all'); // 'all', 'develop', 'deploy'
|
||||
const [itemsPerPage, setItemsPerPage] = useState(15); // 默认桌面端 3行 × 5列 = 15个项目
|
||||
const [itemsPerPage, setItemsPerPage] = useState(12); // 默认桌面端 3行 × 4列 = 12个项目
|
||||
|
||||
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);
|
||||
}
|
||||
setIsAdmin(!!sessionStorage.getItem(ADMIN_TOKEN_KEY));
|
||||
}, [location.key, location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
// 根据屏幕宽度设置每页显示数量
|
||||
const updateItemsPerPage = () => {
|
||||
if (window.innerWidth <= 768) {
|
||||
setItemsPerPage(8); // 移动端:4行 × 2列 = 8个项目
|
||||
} else {
|
||||
setItemsPerPage(15); // 桌面端:3行 × 5列 = 15个项目
|
||||
setItemsPerPage(12); // 桌面端:3行 × 4列 = 12个项目
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,25 +90,64 @@ function ProjectsSection({ projects }) {
|
||||
return () => window.removeEventListener('resize', updateItemsPerPage);
|
||||
}, []);
|
||||
|
||||
// 优先使用 cf-favicon API,失败时 onError 会切到 project.icon 或通用图标
|
||||
// 优先使用 favicon.smyhub.com,失败时 onError 会切到 project.icon 或通用图标
|
||||
const getProjectIconUrl = (link) => {
|
||||
try {
|
||||
new URL(link);
|
||||
return `https://cf-favicon.pages.dev/api/favicon?url=${encodeURIComponent(link)}`;
|
||||
return getFaviconUrlForSite(link);
|
||||
} catch {
|
||||
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');
|
||||
const img = e.currentTarget;
|
||||
if (!img) return;
|
||||
const isApiUrl = img.src && img.src.includes('favicon.smyhub.com');
|
||||
if (isApiUrl && project.icon) {
|
||||
e.target.src = project.icon;
|
||||
img.src = project.icon;
|
||||
} else {
|
||||
e.target.src = 'https://api.iconify.design/mdi:web.svg';
|
||||
img.src = 'https://api.iconify.design/mdi:web.svg';
|
||||
}
|
||||
};
|
||||
|
||||
const handleTechIconError = (e) => {
|
||||
const img = e.currentTarget;
|
||||
if (img) img.style.visibility = 'hidden';
|
||||
};
|
||||
|
||||
const renderTechIcon = (name, key) => {
|
||||
const item = resolveTechItem(name);
|
||||
const iconUrl = item ? getTechstackIconUrl(item) : null;
|
||||
const paint = item?.color || '#64748b';
|
||||
if (item && iconUrl) {
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
className="project-tech-icon"
|
||||
style={techBadgeSurfaceStyle(paint)}
|
||||
title={item.name}
|
||||
>
|
||||
<img src={iconUrl} alt="" loading="lazy" onError={handleTechIconError} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const techNamesForProject = (project) => {
|
||||
const useLegacy = project.techTags === undefined && techNameSet.size > 0;
|
||||
return useLegacy
|
||||
? (project.tags || []).filter((t) => tagIsTech(t))
|
||||
: project.techTags || [];
|
||||
};
|
||||
|
||||
const renderTechRow = (project) => (
|
||||
<div className="project-tech-icons">
|
||||
{techNamesForProject(project).map((n, i) => renderTechIcon(n, `t-${i}`))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 过滤项目
|
||||
// 1. 如果 show 为 false,则不显示
|
||||
// 2. 如果 admin 为 true 且不是管理员模式,则不显示
|
||||
@@ -103,6 +198,14 @@ function ProjectsSection({ projects }) {
|
||||
setCurrentPage(0); // 切换分类时重置到第一页
|
||||
};
|
||||
|
||||
const categoryTagsForProject = (project) => {
|
||||
const useLegacy = project.techTags === undefined && techNameSet.size > 0;
|
||||
if (useLegacy) {
|
||||
return (project.tags || []).filter((t) => !tagIsTech(t));
|
||||
}
|
||||
return project.tags || [];
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="projects-section">
|
||||
<div className="section-header">
|
||||
@@ -147,14 +250,6 @@ function ProjectsSection({ projects }) {
|
||||
onMouseEnter={() => setHoveredIndex(globalIndex)}
|
||||
onMouseLeave={() => setHoveredIndex(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
|
||||
@@ -168,13 +263,25 @@ function ProjectsSection({ projects }) {
|
||||
|
||||
<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>
|
||||
)}
|
||||
{renderTechRow(project)}
|
||||
<div className="project-tags">
|
||||
{categoryTagsForProject(project).map((tag, index) => {
|
||||
const cs = categoryTagStyle(tag);
|
||||
return (
|
||||
<span
|
||||
key={`c-${index}`}
|
||||
className="project-tag project-tag--category"
|
||||
style={{
|
||||
background: cs.background,
|
||||
color: cs.color,
|
||||
borderColor: cs.borderColor,
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
margin-bottom: 40px;
|
||||
animation: fadeInUp 0.8s ease-out 0.15s both;
|
||||
padding: 24px;
|
||||
border: 2px solid rgba(82, 183, 136, 0.3);
|
||||
border: 2px solid var(--accent-alpha-30);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 4px 20px rgba(82, 183, 136, 0.1);
|
||||
box-shadow: 0 4px 20px var(--accent-alpha-10);
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
@@ -77,7 +77,8 @@
|
||||
max-height: 36px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
||||
font-family: 'Inter', 'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
|
||||
'Segoe UI', system-ui, sans-serif;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -136,16 +137,19 @@
|
||||
/* 响应式设计 */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.techstack-section {
|
||||
padding: 16px 10px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.techstack-container {
|
||||
padding: 20px;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
.tech-items {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@@ -179,7 +183,11 @@
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.tech-items {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
.techstack-section {
|
||||
padding: 14px 8px;
|
||||
}
|
||||
|
||||
.techstack-container {
|
||||
padding: 10px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
import React from 'react';
|
||||
import { getTechstackIconUrl, techBadgeSurfaceStyle, techBadgeLabelColor } from '../api';
|
||||
import './TechStackSection.css';
|
||||
|
||||
function TechStackSection({ techstack }) {
|
||||
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 (
|
||||
<section className="techstack-section">
|
||||
<h2 className="section-title">
|
||||
@@ -31,15 +17,15 @@ function TechStackSection({ techstack }) {
|
||||
{techstack.items
|
||||
.filter(item => item.show !== false)
|
||||
.map((item, idx) => {
|
||||
const iconUrl = getIconUrl(item);
|
||||
const backgroundColor = item.color || '#555555';
|
||||
|
||||
const iconUrl = getTechstackIconUrl(item);
|
||||
const badgePaint = item.color || '#555555';
|
||||
|
||||
const badgeContent = (
|
||||
<div
|
||||
className="tech-badge"
|
||||
style={{
|
||||
backgroundColor: backgroundColor,
|
||||
color: backgroundColor === '#FFFFFF' ? '#000000' : 'white'
|
||||
<div
|
||||
className="tech-badge"
|
||||
style={{
|
||||
...techBadgeSurfaceStyle(badgePaint),
|
||||
color: techBadgeLabelColor(badgePaint),
|
||||
}}
|
||||
>
|
||||
<div className="badge-icon">
|
||||
|
||||
129
mengyaprofile-frontend/src/components/admin/AdminModal.css
Normal file
@@ -0,0 +1,129 @@
|
||||
.admin-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 20000;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 宽版弹窗:左右留白略减,让内容区更宽 */
|
||||
.admin-modal-backdrop:has(.admin-modal-panel--wide) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.admin-modal-panel {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
max-height: min(90vh, 720px);
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 项目编辑:表单更宽、更高,便于技术栈多选 */
|
||||
.admin-modal-panel--wide {
|
||||
max-width: min(98vw, 1120px);
|
||||
max-height: min(94vh, 920px);
|
||||
}
|
||||
|
||||
.admin-modal-panel--wide .admin-modal-header,
|
||||
.admin-modal-panel--wide .admin-modal-footer {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.admin-modal-panel--wide .admin-modal-body {
|
||||
padding: 20px 32px;
|
||||
}
|
||||
|
||||
.admin-modal-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.admin-modal-title {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: #5c3d42;
|
||||
}
|
||||
|
||||
.admin-modal-close {
|
||||
border: none;
|
||||
background: #f0f0f0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.admin-modal-close:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.admin-modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.admin-modal-footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #eee;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.admin-modal-footer button {
|
||||
padding: 10px 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.admin-modal-footer button.secondary {
|
||||
background: #e8e8e8;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.admin-modal-footer button.secondary:disabled,
|
||||
.admin-modal-footer button.primary:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.admin-modal-footer button.primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.admin-field-hint {
|
||||
margin: -8px 0 10px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
51
mengyaprofile-frontend/src/components/admin/AdminModal.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import './AdminModal.css';
|
||||
|
||||
export default function AdminModal({ open, title, onClose, children, footer, panelClassName }) {
|
||||
const closeRef = useRef(onClose);
|
||||
closeRef.current = onClose;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'Escape') closeRef.current();
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => document.removeEventListener('keydown', onKey);
|
||||
}, [open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="admin-modal-backdrop"
|
||||
onMouseDown={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={['admin-modal-panel', panelClassName].filter(Boolean).join(' ')}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="admin-modal-title"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="admin-modal-header">
|
||||
<h2 id="admin-modal-title" className="admin-modal-title">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="admin-modal-close"
|
||||
onClick={onClose}
|
||||
aria-label="关闭"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-modal-body">{children}</div>
|
||||
{footer ? <div className="admin-modal-footer">{footer}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,51 @@
|
||||
/*
|
||||
* 暖色主题(淡橙 + 淡粉):全局变量,供 App 与各组件 CSS 引用
|
||||
*/
|
||||
:root {
|
||||
--warm-bg-1: #fff5ef;
|
||||
--warm-bg-2: #ffe8ec;
|
||||
--warm-bg-3: #ffe4d4;
|
||||
--accent: #e8897a;
|
||||
--accent-soft: #f4b8a8;
|
||||
--accent-secondary: #f5c4d0;
|
||||
--accent-link: #b84d5f;
|
||||
--accent-r: 232;
|
||||
--accent-g: 137;
|
||||
--accent-b: 122;
|
||||
--accent-2-r: 245;
|
||||
--accent-2-g: 196;
|
||||
--accent-2-b: 208;
|
||||
--warm-shadow-rgb: 120, 65, 55;
|
||||
|
||||
--warm-gradient-page: linear-gradient(135deg, var(--warm-bg-1) 0%, var(--warm-bg-2) 50%, var(--warm-bg-3) 100%);
|
||||
--warm-gradient-page-pwa: linear-gradient(135deg, var(--warm-bg-1) 0%, var(--warm-bg-2) 40%, var(--warm-bg-3) 100%);
|
||||
--accent-gradient: linear-gradient(135deg, var(--accent), var(--accent-secondary));
|
||||
--accent-gradient-90: linear-gradient(90deg, var(--accent), var(--accent-secondary));
|
||||
|
||||
--accent-alpha-10: rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.1);
|
||||
--accent-alpha-14: rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.14);
|
||||
--accent-alpha-20: rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.2);
|
||||
--accent-alpha-30: rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.3);
|
||||
--accent-alpha-35: rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.35);
|
||||
--accent-alpha-40: rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.4);
|
||||
--accent-alpha-50: rgba(var(--accent-r), var(--accent-g), var(--accent-b), 0.5);
|
||||
--accent-2-alpha-10: rgba(var(--accent-2-r), var(--accent-2-g), var(--accent-2-b), 0.1);
|
||||
--accent-2-alpha-85: rgba(var(--accent-2-r), var(--accent-2-g), var(--accent-2-b), 0.85);
|
||||
--warm-deep-alpha-08: rgba(var(--warm-shadow-rgb), 0.08);
|
||||
--warm-deep-alpha-12: rgba(var(--warm-shadow-rgb), 0.12);
|
||||
--warm-deep-alpha-14: rgba(var(--warm-shadow-rgb), 0.14);
|
||||
--warm-deep-alpha-32: rgba(var(--warm-shadow-rgb), 0.32);
|
||||
}
|
||||
|
||||
/*
|
||||
* 西文:Inter(独立站/产品站常用)
|
||||
* 中文:Noto Sans SC(思源黑体,与 Inter 混排常见)
|
||||
* 回退:系统无衬线,保证无网络时仍可读
|
||||
*/
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-family: 'Inter', 'Noto Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
|
||||
'Segoe UI', system-ui, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
@@ -7,7 +8,9 @@ import reportWebVitals from './reportWebVitals';
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
@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
|
||||