整理项目架构
This commit is contained in:
133
.gitignore
vendored
133
.gitignore
vendored
@@ -1,130 +1,5 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# 项目特定
|
||||
uploads/
|
||||
temp/
|
||||
*.tmp
|
||||
*.bak
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
|
||||
#作品
|
||||
works/
|
||||
frontend/build/
|
||||
frontend/node_modules/
|
||||
SmyWorkCollect-Frontend/build/
|
||||
SmyWorkCollect-Frontend/node_modules/
|
||||
SmyWorkCollect-Backend/__pycache__/
|
||||
SmyWorkCollect-Backend/works/
|
||||
@@ -1,54 +0,0 @@
|
||||
# API配置说明
|
||||
|
||||
## 开发环境
|
||||
开发环境下,前端会自动连接到 `http://localhost:5000/api`,无需额外配置。
|
||||
|
||||
## 生产环境配置
|
||||
|
||||
### 方式1: 前后端同域名部署
|
||||
如果前端和后端部署在同一服务器的同一域名下,使用默认配置即可。
|
||||
前端会使用相对路径 `/api` 访问后端。
|
||||
|
||||
### 方式2: 后端独立部署
|
||||
如果后端部署在不同的服务器或域名,需要设置环境变量:
|
||||
|
||||
1. 在 `frontend` 目录下创建 `.env.local` 文件
|
||||
2. 添加以下内容:
|
||||
```
|
||||
REACT_APP_API_URL=http://your-backend-domain.com:5000/api
|
||||
```
|
||||
|
||||
### 方式3: 修改源代码
|
||||
如果不想使用环境变量,可以直接修改 `src/services/api.js` 和 `src/services/adminApi.js` 文件中的配置。
|
||||
|
||||
## 后端CORS设置
|
||||
确保后端允许前端域名的跨域请求。在 `backend/app.py` 中:
|
||||
|
||||
```python
|
||||
from flask_cors import CORS
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app, origins=['http://your-frontend-domain.com']) # 指定允许的域名
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **构建后无法访问API**: 检查API_URL配置是否正确
|
||||
2. **跨域错误**: 检查后端CORS设置
|
||||
3. **连接超时**: 检查后端服务是否正常运行,防火墙是否开放端口
|
||||
|
||||
## 部署建议
|
||||
|
||||
### 同域名部署 (推荐)
|
||||
```
|
||||
your-domain.com/ -> 前端静态文件
|
||||
your-domain.com/api/ -> 后端API
|
||||
```
|
||||
|
||||
### 不同域名部署
|
||||
```
|
||||
frontend.your-domain.com -> 前端
|
||||
api.your-domain.com -> 后端
|
||||
```
|
||||
|
||||
需要在前端设置 `REACT_APP_API_URL=https://api.your-domain.com/api`
|
||||
181
ShuMengyaWorks-Web/.gitignore
vendored
181
ShuMengyaWorks-Web/.gitignore
vendored
@@ -1,181 +0,0 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor.`.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
@@ -9,6 +9,8 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from werkzeug.utils import secure_filename
|
||||
import tempfile
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -26,9 +28,12 @@ app.config['MAX_FORM_MEMORY_SIZE'] = 1024 * 1024 * 1024 # 1GB
|
||||
app.config['MAX_FORM_PARTS'] = 1000
|
||||
|
||||
# 获取项目根目录
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# works 目录已移动到后端目录下
|
||||
WORKS_DIR = os.path.join(BASE_DIR, 'works')
|
||||
CONFIG_DIR = os.path.join(BASE_DIR, 'config')
|
||||
# config 目录已移动到前端目录下
|
||||
FRONTEND_DIR = os.path.abspath(os.path.join(BASE_DIR, '..', 'SmyWorkCollect-Frontend'))
|
||||
CONFIG_DIR = os.path.join(FRONTEND_DIR, 'config')
|
||||
|
||||
# 管理员token
|
||||
ADMIN_TOKEN = "shumengya520"
|
||||
@@ -50,6 +55,52 @@ RATE_LIMITS = {
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
def safe_filename(filename):
|
||||
"""
|
||||
安全处理文件名,支持中文字符
|
||||
"""
|
||||
if not filename:
|
||||
return ''
|
||||
|
||||
# 保留原始文件名用于显示
|
||||
original_name = filename
|
||||
|
||||
# 规范化Unicode字符
|
||||
filename = unicodedata.normalize('NFKC', filename)
|
||||
|
||||
# 移除或替换危险字符,但保留中文、英文、数字、点、下划线、连字符
|
||||
# 允许的字符:中文字符、英文字母、数字、点、下划线、连字符、空格
|
||||
safe_chars = re.sub(r'[^\w\s\-_.\u4e00-\u9fff]', '', filename)
|
||||
|
||||
# 将多个空格替换为单个下划线
|
||||
safe_chars = re.sub(r'\s+', '_', safe_chars)
|
||||
|
||||
# 移除开头和结尾的点和空格
|
||||
safe_chars = safe_chars.strip('. ')
|
||||
|
||||
# 确保文件名不为空
|
||||
if not safe_chars:
|
||||
return 'unnamed_file'
|
||||
|
||||
# 限制文件名长度(不包括扩展名)
|
||||
name_part, ext_part = os.path.splitext(safe_chars)
|
||||
if len(name_part.encode('utf-8')) > 200: # 限制为200字节
|
||||
# 截断文件名但保持完整的字符
|
||||
name_bytes = name_part.encode('utf-8')[:200]
|
||||
# 确保不会截断中文字符
|
||||
try:
|
||||
name_part = name_bytes.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
# 如果截断位置在中文字符中间,向前查找完整字符
|
||||
for i in range(len(name_bytes) - 1, -1, -1):
|
||||
try:
|
||||
name_part = name_bytes[:i].decode('utf-8')
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
return name_part + ext_part
|
||||
|
||||
def verify_admin_token():
|
||||
"""验证管理员token"""
|
||||
token = request.args.get('token') or request.headers.get('Authorization')
|
||||
@@ -502,7 +553,8 @@ def admin_upload_file(work_id, file_type):
|
||||
logger.error("没有选择文件")
|
||||
return jsonify({'success': False, 'message': '没有选择文件'}), 400
|
||||
|
||||
original_filename = secure_filename(file.filename)
|
||||
# 保存原始文件名(包含中文)
|
||||
original_filename = file.filename
|
||||
logger.info(f"原始文件名: {original_filename}")
|
||||
|
||||
# 检查文件格式
|
||||
@@ -510,7 +562,11 @@ def admin_upload_file(work_id, file_type):
|
||||
logger.error(f"不支持的文件格式: {original_filename}")
|
||||
return jsonify({'success': False, 'message': '不支持的文件格式'}), 400
|
||||
|
||||
file_extension = original_filename.rsplit('.', 1)[1].lower()
|
||||
# 使用安全的文件名处理函数
|
||||
safe_original_filename = safe_filename(original_filename)
|
||||
file_extension = safe_original_filename.rsplit('.', 1)[1].lower() if '.' in safe_original_filename else 'unknown'
|
||||
|
||||
logger.info(f"安全处理后的文件名: {safe_original_filename}")
|
||||
|
||||
# 读取现有配置来生成新文件名
|
||||
config_path = os.path.join(work_dir, 'work_config.json')
|
||||
@@ -525,20 +581,45 @@ def admin_upload_file(work_id, file_type):
|
||||
if file_type == 'image':
|
||||
save_dir = os.path.join(work_dir, 'image')
|
||||
existing_images = config.get('作品截图', [])
|
||||
image_number = len(existing_images) + 1
|
||||
filename = f"image{image_number}.{file_extension}"
|
||||
|
||||
# 尝试使用原始文件名,如果重复则添加序号
|
||||
base_name = safe_original_filename
|
||||
filename = base_name
|
||||
counter = 1
|
||||
while filename in existing_images:
|
||||
name_part, ext_part = os.path.splitext(base_name)
|
||||
filename = f"{name_part}_{counter}{ext_part}"
|
||||
counter += 1
|
||||
|
||||
elif file_type == 'video':
|
||||
save_dir = os.path.join(work_dir, 'video')
|
||||
existing_videos = config.get('作品视频', [])
|
||||
video_number = len(existing_videos) + 1
|
||||
filename = f"video{video_number}.{file_extension}"
|
||||
|
||||
# 尝试使用原始文件名,如果重复则添加序号
|
||||
base_name = safe_original_filename
|
||||
filename = base_name
|
||||
counter = 1
|
||||
while filename in existing_videos:
|
||||
name_part, ext_part = os.path.splitext(base_name)
|
||||
filename = f"{name_part}_{counter}{ext_part}"
|
||||
counter += 1
|
||||
|
||||
elif file_type == 'platform':
|
||||
platform = request.form.get('platform')
|
||||
if not platform:
|
||||
logger.error("平台参数缺失")
|
||||
return jsonify({'success': False, 'message': '平台参数缺失'}), 400
|
||||
save_dir = os.path.join(work_dir, 'platform', platform)
|
||||
filename = f"{work_id}_{platform.lower()}.{file_extension}"
|
||||
|
||||
# 对于平台文件,也尝试保留原始文件名
|
||||
existing_files = config.get('文件名称', {}).get(platform, [])
|
||||
base_name = safe_original_filename
|
||||
filename = base_name
|
||||
counter = 1
|
||||
while filename in existing_files:
|
||||
name_part, ext_part = os.path.splitext(base_name)
|
||||
filename = f"{name_part}_{counter}{ext_part}"
|
||||
counter += 1
|
||||
else:
|
||||
logger.error(f"不支持的文件类型: {file_type}")
|
||||
return jsonify({'success': False, 'message': '不支持的文件类型'}), 400
|
||||
@@ -586,16 +667,25 @@ def admin_upload_file(work_id, file_type):
|
||||
if file_type == 'image':
|
||||
if filename not in config.get('作品截图', []):
|
||||
config.setdefault('作品截图', []).append(filename)
|
||||
# 记录原始文件名映射
|
||||
config.setdefault('原始文件名', {})
|
||||
config['原始文件名'][filename] = original_filename
|
||||
if not config.get('作品封面'):
|
||||
config['作品封面'] = filename
|
||||
elif file_type == 'video':
|
||||
if filename not in config.get('作品视频', []):
|
||||
config.setdefault('作品视频', []).append(filename)
|
||||
# 记录原始文件名映射
|
||||
config.setdefault('原始文件名', {})
|
||||
config['原始文件名'][filename] = original_filename
|
||||
elif file_type == 'platform':
|
||||
platform = request.form.get('platform')
|
||||
config.setdefault('文件名称', {}).setdefault(platform, [])
|
||||
if filename not in config['文件名称'][platform]:
|
||||
config['文件名称'][platform].append(filename)
|
||||
# 记录原始文件名映射
|
||||
config.setdefault('原始文件名', {})
|
||||
config['原始文件名'][filename] = original_filename
|
||||
|
||||
config['更新时间'] = datetime.now().isoformat()
|
||||
|
||||
@@ -679,16 +769,25 @@ def admin_delete_file(work_id, file_type, filename):
|
||||
if file_type == 'image':
|
||||
if filename in config.get('作品截图', []):
|
||||
config['作品截图'].remove(filename)
|
||||
# 清理原始文件名映射
|
||||
if '原始文件名' in config and filename in config['原始文件名']:
|
||||
del config['原始文件名'][filename]
|
||||
if config.get('作品封面') == filename:
|
||||
config['作品封面'] = config['作品截图'][0] if config['作品截图'] else ''
|
||||
elif file_type == 'video':
|
||||
if filename in config.get('作品视频', []):
|
||||
config['作品视频'].remove(filename)
|
||||
# 清理原始文件名映射
|
||||
if '原始文件名' in config and filename in config['原始文件名']:
|
||||
del config['原始文件名'][filename]
|
||||
elif file_type == 'platform':
|
||||
platform = request.args.get('platform')
|
||||
if platform in config.get('文件名称', {}):
|
||||
if filename in config['文件名称'][platform]:
|
||||
config['文件名称'][platform].remove(filename)
|
||||
# 清理原始文件名映射
|
||||
if '原始文件名' in config and filename in config['原始文件名']:
|
||||
del config['原始文件名'][filename]
|
||||
|
||||
config['更新时间'] = datetime.now().isoformat()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@echo off
|
||||
echo 启动树萌芽の作品集后端服务...
|
||||
cd backend
|
||||
echo 正在启动树萌芽の作品集后端...
|
||||
python app.py
|
||||
pause
|
||||
python -m pip install -r requirements.txt
|
||||
1
SmyWorkCollect-Frontend/.env.local
Normal file
1
SmyWorkCollect-Frontend/.env.local
Normal file
@@ -0,0 +1 @@
|
||||
REACT_APP_API_URL=https://work.api.shumengya.top/api
|
||||
@@ -1,5 +1,4 @@
|
||||
@echo off
|
||||
echo 正在构建树萌芽の作品集网站前端
|
||||
cd frontend
|
||||
npm run build
|
||||
pause
|
||||
13
SmyWorkCollect-Frontend/config/settings.json
Normal file
13
SmyWorkCollect-Frontend/config/settings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"网站名字": "✨ 树萌芽の作品集 ✨",
|
||||
"网站描述": "🎨 展示个人制作的一些小创意和小项目,欢迎交流讨论 💬",
|
||||
"站长": "👨💻 by-树萌芽",
|
||||
"联系邮箱": "3205788256@qq.com",
|
||||
"主题颜色": "#81c784",
|
||||
"每页作品数量": 6,
|
||||
"启用搜索": true,
|
||||
"启用分类": true,
|
||||
"备案号": "📄 蜀ICP备2025151694号",
|
||||
"网站页尾": "🌱 树萌芽の作品集 | Copyright © 2025-2025 smy ✨",
|
||||
"网站logo": "assets/logo.png"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 994 B After Width: | Height: | Size: 994 B |
@@ -9,11 +9,48 @@ import SearchBar from './components/SearchBar';
|
||||
import CategoryFilter from './components/CategoryFilter';
|
||||
import LoadingSpinner from './components/LoadingSpinner';
|
||||
import Footer from './components/Footer';
|
||||
import Pagination from './components/Pagination';
|
||||
import { getWorks, getSettings, getCategories, searchWorks } from './services/api';
|
||||
|
||||
const AppContainer = styled.div`
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(232, 245, 232, 0.4) 0%,
|
||||
rgba(200, 230, 201, 0.4) 20%,
|
||||
rgba(165, 214, 167, 0.4) 40%,
|
||||
rgba(255, 255, 224, 0.3) 60%,
|
||||
rgba(255, 255, 200, 0.3) 80%,
|
||||
rgba(240, 255, 240, 0.4) 100%
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gentleShift 25s ease infinite;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(1px);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes gentleShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MainContent = styled.main`
|
||||
@@ -59,12 +96,17 @@ const NoResults = styled.div`
|
||||
`;
|
||||
|
||||
// 首页组件
|
||||
const HomePage = () => {
|
||||
const HomePage = ({ settings }) => {
|
||||
const [works, setWorks] = useState([]);
|
||||
const [allWorks, setAllWorks] = useState([]); // 存储所有作品数据
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
// 从设置中获取每页作品数量,默认为9
|
||||
const itemsPerPage = settings['每页作品数量'] || 9;
|
||||
|
||||
useEffect(() => {
|
||||
loadInitialData();
|
||||
@@ -78,8 +120,11 @@ const HomePage = () => {
|
||||
getCategories()
|
||||
]);
|
||||
|
||||
setWorks(worksData.data || []);
|
||||
const allWorksData = worksData.data || [];
|
||||
setAllWorks(allWorksData);
|
||||
setWorks(allWorksData);
|
||||
setCategories(categoriesData.data || []);
|
||||
setCurrentPage(1); // 重置到第一页
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
} finally {
|
||||
@@ -102,11 +147,14 @@ const HomePage = () => {
|
||||
setLoading(true);
|
||||
if (query || category) {
|
||||
const searchData = await searchWorks(query, category);
|
||||
setAllWorks(searchData.data || []);
|
||||
setWorks(searchData.data || []);
|
||||
} else {
|
||||
const worksData = await getWorks();
|
||||
setAllWorks(worksData.data || []);
|
||||
setWorks(worksData.data || []);
|
||||
}
|
||||
setCurrentPage(1); // 搜索后重置到第一页
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error);
|
||||
} finally {
|
||||
@@ -114,6 +162,19 @@ const HomePage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 分页相关的计算
|
||||
const totalPages = Math.ceil(works.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentWorks = works.slice(startIndex, endIndex);
|
||||
|
||||
// 处理页面变化
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
// 滚动到顶部
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return (
|
||||
<MainContent>
|
||||
<FilterSection>
|
||||
@@ -128,14 +189,23 @@ const HomePage = () => {
|
||||
{loading ? (
|
||||
<LoadingSpinner />
|
||||
) : works.length > 0 ? (
|
||||
<WorksGrid>
|
||||
{works.map((work) => (
|
||||
<WorkCard key={work.作品ID} work={work} />
|
||||
))}
|
||||
</WorksGrid>
|
||||
<>
|
||||
<WorksGrid>
|
||||
{currentWorks.map((work) => (
|
||||
<WorkCard key={work.作品ID} work={work} />
|
||||
))}
|
||||
</WorksGrid>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
totalItems={works.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<NoResults>
|
||||
{searchQuery || selectedCategory ? '没有找到匹配的作品' : '暂无作品'}
|
||||
{searchQuery || selectedCategory ? '🔍 没有找到匹配的作品' : '📝 暂无作品'}
|
||||
</NoResults>
|
||||
)}
|
||||
</MainContent>
|
||||
@@ -163,7 +233,7 @@ function App() {
|
||||
<AppContainer>
|
||||
<Header settings={settings} />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/" element={<HomePage settings={settings} />} />
|
||||
<Route path="/work/:workId" element={<WorkDetail />} />
|
||||
<Route path="/admin" element={<AdminPanel />} />
|
||||
</Routes>
|
||||
223
SmyWorkCollect-Frontend/src/components/Footer.js
Normal file
223
SmyWorkCollect-Frontend/src/components/Footer.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FooterContainer = styled.footer`
|
||||
background: linear-gradient(135deg, #66bb6a 0%, #81c784 30%, #a5d6a7 70%, #c8e6c9 100%);
|
||||
color: #1b5e20;
|
||||
padding: 35px 0 25px;
|
||||
margin-top: 50px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 25px 25px 0 0;
|
||||
box-shadow: 0 -8px 32px rgba(27, 94, 32, 0.15);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #4caf50, #66bb6a, #81c784, #66bb6a, #4caf50);
|
||||
background-size: 200% 100%;
|
||||
animation: flowingTopBorder 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
||||
animation: shimmer 5s infinite;
|
||||
}
|
||||
|
||||
@keyframes flowingTopBorder {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 -12px 40px rgba(27, 94, 32, 0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 25px 0 20px;
|
||||
margin-top: 35px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterContent = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterText = styled.p`
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-shadow: 0 2px 8px rgba(27, 94, 32, 0.3);
|
||||
margin-bottom: 10px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContactInfo = styled.div`
|
||||
margin-bottom: 15px;
|
||||
animation: slideInLeft 0.8s ease-out 0.2s both;
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContactLink = styled.a`
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-decoration: none;
|
||||
text-shadow: 0 2px 8px rgba(27, 94, 32, 0.3);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 1));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
transform: translateY(-1px);
|
||||
|
||||
&::after {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const RecordNumber = styled.p`
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-shadow: 0 2px 8px rgba(27, 94, 32, 0.3);
|
||||
margin-bottom: 5px;
|
||||
animation: slideInRight 0.8s ease-out 0.4s both;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Copyright = styled.p`
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-shadow: 0 2px 8px rgba(27, 94, 32, 0.3);
|
||||
animation: fadeIn 0.8s ease-out 0.6s both;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 0.7; }
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Footer = ({ settings }) => {
|
||||
return (
|
||||
<FooterContainer>
|
||||
<FooterContent>
|
||||
<ContactInfo>
|
||||
<FooterText>
|
||||
📧 联系邮箱: <ContactLink href={`mailto:${settings.联系邮箱}`}>
|
||||
{settings.联系邮箱}
|
||||
</ContactLink>
|
||||
</FooterText>
|
||||
</ContactInfo>
|
||||
|
||||
{settings.备案号 && (
|
||||
<RecordNumber>{settings.备案号}</RecordNumber>
|
||||
)}
|
||||
|
||||
<Copyright>
|
||||
{settings.网站页尾 || '🌱 树萌芽の作品集 | Copyright © 2025 smy ✨'}
|
||||
</Copyright>
|
||||
</FooterContent>
|
||||
</FooterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
242
SmyWorkCollect-Frontend/src/components/Header.js
Normal file
242
SmyWorkCollect-Frontend/src/components/Header.js
Normal file
@@ -0,0 +1,242 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const HeaderContainer = styled.header`
|
||||
/* 头部容器背景渐变:从中绿色到浅绿色的4层渐变 */
|
||||
background: linear-gradient(135deg,rgb(204, 252, 207) 0%,rgb(132, 206, 134) 30%,rgb(157, 216, 159) 70%,rgb(109, 177, 109) 100%);
|
||||
color: #1b5e20; /* 深绿色文字 */
|
||||
padding: 25px 0; /* 上下内边距 */
|
||||
box-shadow: 0 8px 32px rgba(27, 94, 32, 0.15); /* 深绿色阴影效果 */
|
||||
position: relative; /* 相对定位,为伪元素提供定位基准 */
|
||||
overflow: hidden; /* 隐藏溢出内容,确保动画效果不会超出边界 */
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); /* 平滑过渡动画 */
|
||||
border-radius: 0 0 25px 25px; /* 底部圆角,营造圆润效果 */
|
||||
margin-bottom: 10px; /* 与下方内容的间距 */
|
||||
|
||||
/* 光泽动画效果:从左到右的白色光泽扫过 */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%; /* 初始位置在左侧外部 */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* 半透明白色渐变,中间较亮 */
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
|
||||
animation: shimmer 4s infinite; /* 4秒循环的光泽动画 */
|
||||
}
|
||||
|
||||
/* 底部流动边框:彩色边框从左到右流动 */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px; /* 边框高度 */
|
||||
/* 绿色系渐变边框 */
|
||||
background: linear-gradient(90deg,rgb(21, 221, 31),rgb(2, 233, 14),rgb(0, 161, 5),rgb(0, 25rgb(12, 221, 23)#66bb6a);
|
||||
background-size: 200% 100%; /* 背景尺寸为200%,用于动画效果 */
|
||||
animation: flowingBorder 3s ease-in-out infinite; /* 3秒循环的流动动画 */
|
||||
}
|
||||
|
||||
/* 光泽扫过动画:从左侧移动到右侧 */
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; } /* 开始位置:左侧外部 */
|
||||
100% { left: 100%; } /* 结束位置:右侧外部 */
|
||||
}
|
||||
|
||||
/* 边框流动动画:背景位置左右移动 */
|
||||
@keyframes flowingBorder {
|
||||
0%, 100% { background-position: 0% 50%; } /* 起始和结束位置 */
|
||||
50% { background-position: 100% 50%; } /* 中间位置 */
|
||||
}
|
||||
|
||||
/* 悬停效果:增强阴影和轻微上移 */
|
||||
&:hover {
|
||||
box-shadow: 0 12px 40px rgba(27, 94, 32, 0.2); /* 更深的阴影 */
|
||||
transform: translateY(-2px); /* 向上移动2像素 */
|
||||
}
|
||||
`;
|
||||
|
||||
const HeaderContent = styled.div`
|
||||
max-width: 1200px; /* 最大宽度限制 */
|
||||
margin: 0 auto; /* 水平居中 */
|
||||
padding: 0 20px; /* 左右内边距 */
|
||||
text-align: center; /* 文字居中对齐 */
|
||||
display: flex; /* 弹性布局 */
|
||||
flex-direction: column; /* 垂直排列 */
|
||||
align-items: center; /* 子元素居中对齐 */
|
||||
|
||||
/* 移动端响应式:减少左右内边距 */
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
margin-bottom: 15px; /* 底部间距 */
|
||||
animation: fadeInUp 0.8s ease-out; /* 淡入向上动画 */
|
||||
|
||||
/* Logo淡入动画:从下方淡入 */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0; /* 初始透明 */
|
||||
transform: translateY(20px); /* 初始向下偏移 */
|
||||
}
|
||||
to {
|
||||
opacity: 1; /* 最终不透明 */
|
||||
transform: translateY(0); /* 最终正常位置 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端响应式:减少底部间距 */
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Logo = styled.img`
|
||||
height: 80px; /* Logo高度 */
|
||||
width: auto; /* 宽度自适应,保持比例 */
|
||||
border-radius: 12px; /* 圆角效果 */
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); /* 平滑过渡动画 */
|
||||
filter: drop-shadow(0 2px 8px rgba(46, 93, 49, 0.15)); /* 投影效果 */
|
||||
|
||||
/* Logo悬停效果:放大并轻微旋转 */
|
||||
&:hover {
|
||||
transform: scale(1.05) rotate(2deg); /* 放大105%并旋转2度 */
|
||||
filter: drop-shadow(0 4px 12px rgba(46, 93, 49, 0.25)); /* 增强投影 */
|
||||
}
|
||||
|
||||
/* 移动端响应式:减小Logo尺寸 */
|
||||
@media (max-width: 768px) {
|
||||
height: 60px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
font-size: 3rem; /* 标题字体大小 */
|
||||
margin-bottom: 10px; /* 底部间距 */
|
||||
font-weight: 700; /* 字体粗细 */
|
||||
position: relative; /* 相对定位,为伪元素提供基准 */
|
||||
|
||||
/* 文字颜色:纯白色,保持清晰可读 */
|
||||
color: #ffffff;
|
||||
|
||||
/* 金色描边效果:使用-webkit-text-stroke创建外描边 */
|
||||
-webkit-text-stroke: 2px #ffd700; /* 2像素金色描边 */
|
||||
text-stroke: 2px #ffd700; /* 标准属性 */
|
||||
|
||||
/* 外围辐射金光:只在外围产生光晕,不影响文字内部 */
|
||||
filter: drop-shadow(0 0 4px #ffd700)
|
||||
drop-shadow(0 0 8px #ffd700)
|
||||
drop-shadow(0 0 12px #ffed4e);
|
||||
|
||||
/* 底部立体阴影 */
|
||||
text-shadow: 0 3px 6px rgba(0,0,0,0.3);
|
||||
|
||||
/* 淡入向上动画 + 金光闪烁效果 */
|
||||
animation:
|
||||
fadeInUp 0.8s ease-out 0.2s both, /* 淡入向上动画 */
|
||||
goldGlow 3s ease-in-out infinite; /* 金光闪烁效果 */
|
||||
|
||||
|
||||
|
||||
/* 淡入向上动画 */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0; /* 初始透明 */
|
||||
transform: translateY(20px); /* 初始位置向下偏移 */
|
||||
}
|
||||
to {
|
||||
opacity: 1; /* 最终不透明 */
|
||||
transform: translateY(0); /* 最终位置正常 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计:移动端字体大小调整 */
|
||||
@media (max-width: 768px) {
|
||||
font-size: 2.5rem; /* 移动端较小字体 */
|
||||
-webkit-text-stroke: 1.5px #ffd700; /* 移动端较细描边 */
|
||||
|
||||
/* 移动端减弱光晕效果,避免性能问题 */
|
||||
filter: drop-shadow(0 0 6px #ffd700)
|
||||
drop-shadow(0 0 12px #ffed4e);
|
||||
}
|
||||
`;
|
||||
|
||||
const Description = styled.p`
|
||||
font-size: 1.1rem; /* 描述文字大小 */
|
||||
color: rgba(255, 255, 255, 0.9); /* 半透明白色文字 */
|
||||
text-shadow: 0 2px 8px rgba(27, 94, 32, 0.3); /* 绿色文字阴影 */
|
||||
margin-bottom: 5px; /* 底部间距 */
|
||||
animation: fadeInUp 0.8s ease-out 0.4s both; /* 延迟0.4秒的淡入动画 */
|
||||
transition: all 0.3s ease; /* 平滑过渡效果 */
|
||||
|
||||
/* 描述文字悬停效果:变为完全不透明并上移 */
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 1); /* 完全不透明的白色 */
|
||||
transform: translateY(-2px); /* 向上移动2像素 */
|
||||
}
|
||||
|
||||
/* 移动端响应式:减小字体大小 */
|
||||
@media (max-width: 768px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Author = styled.p`
|
||||
font-size: 0.9rem; /* 作者信息字体大小 */
|
||||
color: rgba(255, 255, 255, 0.8); /* 更透明的白色文字 */
|
||||
text-shadow: 0 2px 8px rgba(27, 94, 32, 0.3); /* 绿色文字阴影 */
|
||||
animation: fadeInUp 0.8s ease-out 0.6s both; /* 延迟0.6秒的淡入动画 */
|
||||
transition: all 0.3s ease; /* 平滑过渡效果 */
|
||||
|
||||
/* 作者信息悬停效果:变为完全不透明并上移 */
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 1); /* 完全不透明的白色 */
|
||||
transform: translateY(-2px); /* 向上移动2像素 */
|
||||
}
|
||||
`;
|
||||
|
||||
const Header = ({ settings }) => {
|
||||
// 动态设置favicon
|
||||
React.useEffect(() => {
|
||||
if (settings.网站logo) {
|
||||
const favicon = document.querySelector('link[rel="icon"]');
|
||||
if (favicon) {
|
||||
favicon.href = settings.网站logo;
|
||||
} else {
|
||||
// 如果没有favicon链接,创建一个
|
||||
const newFavicon = document.createElement('link');
|
||||
newFavicon.rel = 'icon';
|
||||
newFavicon.href = settings.网站logo;
|
||||
document.head.appendChild(newFavicon);
|
||||
}
|
||||
}
|
||||
}, [settings.网站logo]);
|
||||
|
||||
return (
|
||||
<HeaderContainer>
|
||||
<HeaderContent>
|
||||
{settings.网站logo && (
|
||||
<LogoContainer>
|
||||
<Logo
|
||||
src={settings.网站logo}
|
||||
alt={settings.网站名字 || '树萌芽の作品集'}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</LogoContainer>
|
||||
)}
|
||||
<Title>{settings.网站名字 || '树萌芽の作品集'}</Title>
|
||||
<Description>{settings.网站描述 || '展示我的创意作品和项目'}</Description>
|
||||
<Author>{settings.站长 || '树萌芽'}</Author>
|
||||
</HeaderContent>
|
||||
</HeaderContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
161
SmyWorkCollect-Frontend/src/components/Pagination.js
Normal file
161
SmyWorkCollect-Frontend/src/components/Pagination.js
Normal file
@@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const PaginationContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 30px 0;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const PaginationButton = styled.button`
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #81c784;
|
||||
background: ${props => props.active ? '#81c784' : 'rgba(255, 255, 255, 0.9)'};
|
||||
color: ${props => props.active ? 'white' : '#2e7d32'};
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 40px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: ${props => props.active ? '#66bb6a' : '#e8f5e8'};
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(129, 199, 132, 0.3);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
min-width: 35px;
|
||||
}
|
||||
`;
|
||||
|
||||
const PageInfo = styled.div`
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 0 10px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 12px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Ellipsis = styled.span`
|
||||
color: #666;
|
||||
padding: 0 5px;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const Pagination = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems,
|
||||
itemsPerPage,
|
||||
onPageChange
|
||||
}) => {
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
const maxVisiblePages = 7; // 最多显示7个页码按钮
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
// 如果总页数小于等于最大显示页数,显示所有页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// 复杂的分页逻辑
|
||||
if (currentPage <= 4) {
|
||||
// 当前页在前面
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push('...');
|
||||
pages.push(totalPages);
|
||||
} else if (currentPage >= totalPages - 3) {
|
||||
// 当前页在后面
|
||||
pages.push(1);
|
||||
pages.push('...');
|
||||
for (let i = totalPages - 4; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// 当前页在中间
|
||||
pages.push(1);
|
||||
pages.push('...');
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push('...');
|
||||
pages.push(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
const handlePageClick = (page) => {
|
||||
if (page !== '...' && page !== currentPage && page >= 1 && page <= totalPages) {
|
||||
onPageChange(page);
|
||||
}
|
||||
};
|
||||
|
||||
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||
|
||||
return (
|
||||
<PaginationContainer>
|
||||
{/* 上一页按钮 */}
|
||||
<PaginationButton
|
||||
onClick={() => handlePageClick(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
← 上一页
|
||||
</PaginationButton>
|
||||
|
||||
{/* 页码按钮 */}
|
||||
{getPageNumbers().map((page, index) => (
|
||||
page === '...' ? (
|
||||
<Ellipsis key={`ellipsis-${index}`}>...</Ellipsis>
|
||||
) : (
|
||||
<PaginationButton
|
||||
key={page}
|
||||
active={page === currentPage}
|
||||
onClick={() => handlePageClick(page)}
|
||||
>
|
||||
{page}
|
||||
</PaginationButton>
|
||||
)
|
||||
))}
|
||||
|
||||
{/* 下一页按钮 */}
|
||||
<PaginationButton
|
||||
onClick={() => handlePageClick(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
下一页 →
|
||||
</PaginationButton>
|
||||
|
||||
{/* 页面信息 */}
|
||||
<PageInfo>
|
||||
第 {startItem}-{endItem} 项,共 {totalItems} 项
|
||||
</PageInfo>
|
||||
</PaginationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
@@ -14,16 +14,18 @@ const getApiBaseUrl = () => {
|
||||
};
|
||||
|
||||
const Card = styled.div`
|
||||
background: white;
|
||||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.95), rgba(248, 255, 248, 0.95));
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(129, 199, 132, 0.2);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 8px 30px rgba(129, 199, 132, 0.2);
|
||||
border-color: rgba(129, 199, 132, 0.4);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -192,7 +194,7 @@ const WorkCard = ({ work }) => {
|
||||
/>
|
||||
) : null}
|
||||
<ImagePlaceholder style={{ display: getCoverImage() ? 'none' : 'flex' }}>
|
||||
🎮
|
||||
🎨
|
||||
</ImagePlaceholder>
|
||||
</ImageContainer>
|
||||
|
||||
@@ -212,13 +214,13 @@ const WorkCard = ({ work }) => {
|
||||
)}
|
||||
|
||||
<InfoRow>
|
||||
<span>作者: {work.作者}</span>
|
||||
<span>v{work.作品版本号}</span>
|
||||
<span>👨💻 作者: {work.作者}</span>
|
||||
<span>🏷️ v{work.作品版本号}</span>
|
||||
</InfoRow>
|
||||
|
||||
<InfoRow>
|
||||
<span>分类: {work.作品分类}</span>
|
||||
<span>{formatDate(work.更新时间)}</span>
|
||||
<span>📂 分类: {work.作品分类}</span>
|
||||
<span>📅 {formatDate(work.更新时间)}</span>
|
||||
</InfoRow>
|
||||
|
||||
{work.支持平台 && work.支持平台.length > 0 && (
|
||||
@@ -231,25 +233,25 @@ const WorkCard = ({ work }) => {
|
||||
|
||||
<StatsContainer>
|
||||
<StatItem>
|
||||
<StatIcon>浏览量</StatIcon>
|
||||
<StatIcon>👀</StatIcon>
|
||||
<StatValue>{work.作品浏览量 || 0}</StatValue>
|
||||
</StatItem>
|
||||
<StatItem>
|
||||
<StatIcon>下载量</StatIcon>
|
||||
<StatIcon>📥</StatIcon>
|
||||
<StatValue>{work.作品下载量 || 0}</StatValue>
|
||||
</StatItem>
|
||||
<StatItem>
|
||||
<StatIcon>点赞数</StatIcon>
|
||||
<StatIcon>💖</StatIcon>
|
||||
<StatValue>{work.作品点赞量 || 0}</StatValue>
|
||||
</StatItem>
|
||||
<StatItem>
|
||||
<StatIcon>更新次数</StatIcon>
|
||||
<StatIcon>🔄</StatIcon>
|
||||
<StatValue>{work.作品更新次数 || 0}</StatValue>
|
||||
</StatItem>
|
||||
</StatsContainer>
|
||||
|
||||
<ViewDetailText>
|
||||
点击查看详情 →
|
||||
🌟 点击查看详情 →
|
||||
</ViewDetailText>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -19,6 +19,7 @@ const DetailContainer = styled.div`
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
@@ -26,18 +27,25 @@ const DetailContainer = styled.div`
|
||||
`;
|
||||
|
||||
const BackButton = styled.button`
|
||||
background: #81c784;
|
||||
background: linear-gradient(45deg, #81c784, #66bb6a);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
transition: background 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(129, 199, 132, 0.3);
|
||||
|
||||
&:before {
|
||||
content: '🏠 ';
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #66bb6a;
|
||||
background: linear-gradient(45deg, #66bb6a, #4caf50);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(129, 199, 132, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -265,8 +273,28 @@ const StatsSection = styled.div`
|
||||
margin: 20px 0;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
gap: 10px;
|
||||
padding-bottom: 5px;
|
||||
|
||||
/* 添加滚动条样式 */
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #81c784;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -285,6 +313,9 @@ const StatCard = styled.div`
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 12px;
|
||||
min-width: 80px;
|
||||
flex: 1 0 auto;
|
||||
margin-right: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -319,7 +350,18 @@ const StatLabel = styled.div`
|
||||
`;
|
||||
|
||||
const LikeButton = styled.button`
|
||||
background: ${props => props.liked ? '#4caf50' : '#81c784'};
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
rgba(255, 107, 107, 0.8),
|
||||
rgba(255, 165, 0, 0.8),
|
||||
rgba(255, 255, 0, 0.7),
|
||||
rgba(50, 205, 50, 0.8),
|
||||
rgba(0, 191, 255, 0.8),
|
||||
rgba(65, 105, 225, 0.8),
|
||||
rgba(147, 112, 219, 0.8)
|
||||
);
|
||||
background-size: 300% 300%;
|
||||
animation: buttonRainbow 12s ease infinite;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
@@ -331,10 +373,11 @@ const LikeButton = styled.button`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: ${props => props.liked ? '#45a049' : '#66bb6a'};
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
@@ -345,6 +388,16 @@ const LikeButton = styled.button`
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes buttonRainbow {
|
||||
0%, 100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -363,6 +416,98 @@ const ErrorMessage = styled.div`
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
`;
|
||||
|
||||
// 模态框样式
|
||||
const ModalOverlay = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModalContent = styled.div`
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
`;
|
||||
|
||||
const ModalImage = styled.img`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 85vh;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const ModalVideo = styled.video`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 85vh;
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1001;
|
||||
transition: background 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
font-size: 18px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModalTitle = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||
color: white;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
z-index: 1001;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
const WorkDetail = () => {
|
||||
const { workId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
@@ -371,6 +516,12 @@ const WorkDetail = () => {
|
||||
const [error, setError] = useState(null);
|
||||
const [liking, setLiking] = useState(false);
|
||||
const [likeMessage, setLikeMessage] = useState('');
|
||||
|
||||
// 模态框状态
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalType, setModalType] = useState(''); // 'image' 或 'video'
|
||||
const [modalSrc, setModalSrc] = useState('');
|
||||
const [modalTitle, setModalTitle] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkDetail();
|
||||
@@ -403,10 +554,56 @@ const WorkDetail = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleImageClick = (imageUrl) => {
|
||||
window.open(`${getApiBaseUrl()}${imageUrl}`, '_blank');
|
||||
// 打开图片模态框
|
||||
const handleImageClick = (imageUrl, index) => {
|
||||
setModalType('image');
|
||||
setModalSrc(`${getApiBaseUrl()}${imageUrl}`);
|
||||
setModalTitle(`${work.作品作品} - 截图 ${index + 1}`);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
// 打开视频模态框
|
||||
const handleVideoClick = (videoUrl, index) => {
|
||||
setModalType('video');
|
||||
setModalSrc(`${getApiBaseUrl()}${videoUrl}`);
|
||||
setModalTitle(`${work.作品作品} - 视频 ${index + 1}`);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
// 关闭模态框
|
||||
const closeModal = () => {
|
||||
setModalOpen(false);
|
||||
setModalType('');
|
||||
setModalSrc('');
|
||||
setModalTitle('');
|
||||
};
|
||||
|
||||
// 处理模态框背景点击
|
||||
const handleModalOverlayClick = (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理键盘事件
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === 'Escape' && modalOpen) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
if (modalOpen) {
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
document.body.style.overflow = 'hidden'; // 防止背景滚动
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, [modalOpen]);
|
||||
|
||||
const handleLike = async () => {
|
||||
if (liking) return;
|
||||
|
||||
@@ -447,7 +644,7 @@ const WorkDetail = () => {
|
||||
return (
|
||||
<DetailContainer>
|
||||
<BackButton onClick={() => navigate('/')}>
|
||||
← 返回首页
|
||||
返回首页
|
||||
</BackButton>
|
||||
<ErrorMessage>{error}</ErrorMessage>
|
||||
</DetailContainer>
|
||||
@@ -458,7 +655,7 @@ const WorkDetail = () => {
|
||||
return (
|
||||
<DetailContainer>
|
||||
<BackButton onClick={() => navigate('/')}>
|
||||
← 返回首页
|
||||
返回首页
|
||||
</BackButton>
|
||||
<ErrorMessage>作品不存在</ErrorMessage>
|
||||
</DetailContainer>
|
||||
@@ -476,19 +673,19 @@ const WorkDetail = () => {
|
||||
|
||||
<WorkMeta>
|
||||
<MetaItem>
|
||||
<strong>作者:</strong> {work.作者}
|
||||
<strong>👨💻 作者:</strong> {work.作者}
|
||||
</MetaItem>
|
||||
<MetaItem>
|
||||
<strong>版本:</strong> {work.作品版本号}
|
||||
<strong>🏷️ 版本:</strong> {work.作品版本号}
|
||||
</MetaItem>
|
||||
<MetaItem>
|
||||
<strong>分类:</strong> {work.作品分类}
|
||||
<strong>📂 分类:</strong> {work.作品分类}
|
||||
</MetaItem>
|
||||
<MetaItem>
|
||||
<strong>上传时间:</strong> {formatDate(work.上传时间)}
|
||||
<strong>📅 上传时间:</strong> {formatDate(work.上传时间)}
|
||||
</MetaItem>
|
||||
<MetaItem>
|
||||
<strong>更新时间:</strong> {formatDate(work.更新时间)}
|
||||
<strong>🔄 更新时间:</strong> {formatDate(work.更新时间)}
|
||||
</MetaItem>
|
||||
</WorkMeta>
|
||||
|
||||
@@ -513,17 +710,17 @@ const WorkDetail = () => {
|
||||
{/* 统计数据 */}
|
||||
<StatsSection>
|
||||
<StatCard>
|
||||
<StatIcon>👁️</StatIcon>
|
||||
<StatIcon>👁️🗨️</StatIcon>
|
||||
<StatValue>{work.作品浏览量 || 0}</StatValue>
|
||||
<StatLabel>浏览量</StatLabel>
|
||||
</StatCard>
|
||||
<StatCard>
|
||||
<StatIcon>⬇️</StatIcon>
|
||||
<StatIcon>📥</StatIcon>
|
||||
<StatValue>{work.作品下载量 || 0}</StatValue>
|
||||
<StatLabel>下载量</StatLabel>
|
||||
</StatCard>
|
||||
<StatCard>
|
||||
<StatIcon>👍</StatIcon>
|
||||
<StatIcon>💖</StatIcon>
|
||||
<StatValue>{work.作品点赞量 || 0}</StatValue>
|
||||
<StatLabel>点赞量</StatLabel>
|
||||
</StatCard>
|
||||
@@ -544,8 +741,8 @@ const WorkDetail = () => {
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<span>👍</span>
|
||||
{liking ? '点赞中...' : '点赞作品'}
|
||||
<span>💖</span>
|
||||
{liking ? '💫 点赞中...' : '点赞作品'}
|
||||
{likeMessage && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -567,10 +764,14 @@ const WorkDetail = () => {
|
||||
|
||||
{work.视频链接 && work.视频链接.length > 0 && (
|
||||
<ContentSection>
|
||||
<SectionTitle>作品视频</SectionTitle>
|
||||
<SectionTitle>🎬 作品视频</SectionTitle>
|
||||
{work.视频链接.map((videoUrl, index) => (
|
||||
<VideoContainer key={index}>
|
||||
<VideoPlayer controls>
|
||||
<VideoPlayer
|
||||
controls
|
||||
onClick={() => handleVideoClick(videoUrl, index)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<source src={`${getApiBaseUrl()}${videoUrl}`} type="video/mp4" />
|
||||
您的浏览器不支持视频播放
|
||||
</VideoPlayer>
|
||||
@@ -579,28 +780,9 @@ const WorkDetail = () => {
|
||||
</ContentSection>
|
||||
)}
|
||||
|
||||
{work.图片链接 && work.图片链接.length > 0 && (
|
||||
<ContentSection>
|
||||
<SectionTitle>作品截图</SectionTitle>
|
||||
<ImageGallery>
|
||||
{work.图片链接.map((imageUrl, index) => (
|
||||
<WorkImage
|
||||
key={index}
|
||||
src={`${getApiBaseUrl()}${imageUrl}`}
|
||||
alt={`${work.作品作品} 截图 ${index + 1}`}
|
||||
onClick={() => handleImageClick(imageUrl)}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ImageGallery>
|
||||
</ContentSection>
|
||||
)}
|
||||
|
||||
{work.下载链接 && Object.keys(work.下载链接).length > 0 && (
|
||||
<ContentSection>
|
||||
<SectionTitle>下载作品</SectionTitle>
|
||||
<SectionTitle>📦 下载作品</SectionTitle>
|
||||
<DownloadSection>
|
||||
{Object.entries(work.下载链接).map(([platform, links]) => (
|
||||
<PlatformDownload key={platform}>
|
||||
@@ -611,7 +793,7 @@ const WorkDetail = () => {
|
||||
href={`${getApiBaseUrl()}${link}`}
|
||||
download
|
||||
>
|
||||
下载 {platform} 版本
|
||||
📥 下载 {platform} 版本
|
||||
</DownloadButton>
|
||||
))}
|
||||
</PlatformDownload>
|
||||
@@ -619,6 +801,53 @@ const WorkDetail = () => {
|
||||
</DownloadSection>
|
||||
</ContentSection>
|
||||
)}
|
||||
|
||||
{work.图片链接 && work.图片链接.length > 0 && (
|
||||
<ContentSection>
|
||||
<SectionTitle>🖼️ 作品截图</SectionTitle>
|
||||
<ImageGallery>
|
||||
{work.图片链接.map((imageUrl, index) => (
|
||||
<WorkImage
|
||||
key={index}
|
||||
src={`${getApiBaseUrl()}${imageUrl}`}
|
||||
alt={`${work.作品作品} 截图 ${index + 1}`}
|
||||
onClick={() => handleImageClick(imageUrl, index)}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ImageGallery>
|
||||
</ContentSection>
|
||||
)}
|
||||
|
||||
{/* 模态框 */}
|
||||
{modalOpen && (
|
||||
<ModalOverlay onClick={handleModalOverlayClick}>
|
||||
<ModalContent>
|
||||
<CloseButton onClick={closeModal}>×</CloseButton>
|
||||
{modalType === 'image' ? (
|
||||
<ModalImage
|
||||
src={modalSrc}
|
||||
alt={modalTitle}
|
||||
onError={(e) => {
|
||||
console.error('图片加载失败:', modalSrc);
|
||||
}}
|
||||
/>
|
||||
) : modalType === 'video' ? (
|
||||
<ModalVideo
|
||||
src={modalSrc}
|
||||
controls
|
||||
autoPlay
|
||||
onError={(e) => {
|
||||
console.error('视频加载失败:', modalSrc);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<ModalTitle>{modalTitle}</ModalTitle>
|
||||
</ModalContent>
|
||||
</ModalOverlay>
|
||||
)}
|
||||
</DetailContainer>
|
||||
);
|
||||
};
|
||||
5
SmyWorkCollect-Frontend/start_frontend.bat
Normal file
5
SmyWorkCollect-Frontend/start_frontend.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
echo 正在启动树萌芽の作品集前端...
|
||||
npm start
|
||||
pause
|
||||
npm install
|
||||
192
SmyWorkCollect-Frontend/test/background.css
Normal file
192
SmyWorkCollect-Frontend/test/background.css
Normal file
@@ -0,0 +1,192 @@
|
||||
/* 彩虹渐变背景样式 */
|
||||
|
||||
/* 主背景渐变 */
|
||||
body {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 107, 107, 0.3) 0%,
|
||||
rgba(255, 165, 0, 0.3) 14.28%,
|
||||
rgba(255, 255, 0, 0.25) 28.56%,
|
||||
rgba(50, 205, 50, 0.3) 42.84%,
|
||||
rgba(0, 191, 255, 0.3) 57.12%,
|
||||
rgba(65, 105, 225, 0.3) 71.4%,
|
||||
rgba(147, 112, 219, 0.3) 85.68%,
|
||||
rgba(255, 105, 180, 0.3) 100%
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: rainbowShift 20s ease infinite;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 彩虹渐变动画 */
|
||||
@keyframes rainbowShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 半透明覆盖层,增强可读性 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 搜索按钮彩虹渐变 */
|
||||
.search-btn {
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
rgba(255, 107, 107, 0.8),
|
||||
rgba(255, 165, 0, 0.8),
|
||||
rgba(255, 255, 0, 0.7),
|
||||
rgba(50, 205, 50, 0.8),
|
||||
rgba(0, 191, 255, 0.8),
|
||||
rgba(65, 105, 225, 0.8),
|
||||
rgba(147, 112, 219, 0.8)
|
||||
);
|
||||
background-size: 300% 300%;
|
||||
animation: buttonRainbow 12s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes buttonRainbow {
|
||||
0%, 100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 结果卡片边框彩虹渐变 */
|
||||
.result-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
rgba(255, 107, 107, 0.4),
|
||||
rgba(255, 165, 0, 0.4),
|
||||
rgba(255, 255, 0, 0.3),
|
||||
rgba(50, 205, 50, 0.4),
|
||||
rgba(0, 191, 255, 0.4),
|
||||
rgba(65, 105, 225, 0.4),
|
||||
rgba(147, 112, 219, 0.4),
|
||||
rgba(255, 107, 107, 0.4)
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: borderRainbow 15s linear infinite;
|
||||
border-radius: inherit;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes borderRainbow {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 400% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画彩虹效果 */
|
||||
.loading-spinner {
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 4px solid transparent;
|
||||
border-image: linear-gradient(
|
||||
45deg,
|
||||
#ff6b6b,
|
||||
#ffa500,
|
||||
#ffff00,
|
||||
#32cd32,
|
||||
#00bfff,
|
||||
#4169e1,
|
||||
#9370db
|
||||
) 1;
|
||||
animation: spin 1s linear infinite, colorShift 3s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes colorShift {
|
||||
0%, 100% {
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
filter: hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 链接悬停彩虹效果 */
|
||||
.result-link:hover {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 107, 107, 0.7),
|
||||
rgba(255, 165, 0, 0.7),
|
||||
rgba(255, 255, 0, 0.6),
|
||||
rgba(50, 205, 50, 0.7),
|
||||
rgba(0, 191, 255, 0.7),
|
||||
rgba(65, 105, 225, 0.7),
|
||||
rgba(147, 112, 219, 0.7)
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
animation: linkRainbow 3s ease infinite;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
@keyframes linkRainbow {
|
||||
0%, 100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 标题彩虹文字效果 */
|
||||
.title {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 107, 107, 0.8),
|
||||
rgba(255, 165, 0, 0.8),
|
||||
rgba(255, 255, 0, 0.7),
|
||||
rgba(50, 205, 50, 0.8),
|
||||
rgba(0, 191, 255, 0.8),
|
||||
rgba(65, 105, 225, 0.8),
|
||||
rgba(147, 112, 219, 0.8)
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
animation: titleRainbow 8s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes titleRainbow {
|
||||
0%, 100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"网站名字": "树萌芽の作品集",
|
||||
"网站描述": "展示个人制作的一些小创意和小项目,欢迎交流讨论",
|
||||
"站长": "by-树萌芽",
|
||||
"联系邮箱": "3205788256@qq.com",
|
||||
"主题颜色": "#81c784",
|
||||
"每页作品数量": 9,
|
||||
"启用搜索": true,
|
||||
"启用分类": true,
|
||||
"备案号": "蜀ICP备2025151694号",
|
||||
"网站页尾": "树萌芽の作品集 | Copyright © 2025-2025 smy",
|
||||
"网站logo": "assets/logo.png"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
REACT_APP_API_URL=http://127.0.0.1:5000/api
|
||||
@@ -1,101 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FooterContainer = styled.footer`
|
||||
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
|
||||
color: white;
|
||||
padding: 30px 0 20px;
|
||||
margin-top: 40px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 20px 0 15px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterContent = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterText = styled.p`
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContactInfo = styled.div`
|
||||
margin-bottom: 15px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContactLink = styled.a`
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const RecordNumber = styled.p`
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 5px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Copyright = styled.p`
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Footer = ({ settings }) => {
|
||||
return (
|
||||
<FooterContainer>
|
||||
<FooterContent>
|
||||
<ContactInfo>
|
||||
<FooterText>
|
||||
联系邮箱: <ContactLink href={`mailto:${settings.联系邮箱}`}>
|
||||
{settings.联系邮箱}
|
||||
</ContactLink>
|
||||
</FooterText>
|
||||
</ContactInfo>
|
||||
|
||||
{settings.备案号 && (
|
||||
<RecordNumber>{settings.备案号}</RecordNumber>
|
||||
)}
|
||||
|
||||
<Copyright>
|
||||
{settings.网站页尾 || '树萌芽の作品集 | Copyright © 2025 smy'}
|
||||
</Copyright>
|
||||
</FooterContent>
|
||||
</FooterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -1,108 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const HeaderContainer = styled.header`
|
||||
background: linear-gradient(135deg, #81c784 0%, #a5d6a7 100%);
|
||||
color: white;
|
||||
padding: 20px 0;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
`;
|
||||
|
||||
const HeaderContent = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
margin-bottom: 15px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Logo = styled.img`
|
||||
height: 60px;
|
||||
width: auto;
|
||||
border-radius: 8px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
height: 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Description = styled.p`
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 5px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Author = styled.p`
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
`;
|
||||
|
||||
const Header = ({ settings }) => {
|
||||
// 动态设置favicon
|
||||
React.useEffect(() => {
|
||||
if (settings.网站logo) {
|
||||
const favicon = document.querySelector('link[rel="icon"]');
|
||||
if (favicon) {
|
||||
favicon.href = settings.网站logo;
|
||||
} else {
|
||||
// 如果没有favicon链接,创建一个
|
||||
const newFavicon = document.createElement('link');
|
||||
newFavicon.rel = 'icon';
|
||||
newFavicon.href = settings.网站logo;
|
||||
document.head.appendChild(newFavicon);
|
||||
}
|
||||
}
|
||||
}, [settings.网站logo]);
|
||||
|
||||
return (
|
||||
<HeaderContainer>
|
||||
<HeaderContent>
|
||||
{settings.网站logo && (
|
||||
<LogoContainer>
|
||||
<Logo
|
||||
src={settings.网站logo}
|
||||
alt={settings.网站名字 || '树萌芽の作品集'}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</LogoContainer>
|
||||
)}
|
||||
<Title>{settings.网站名字 || '树萌芽の作品集'}</Title>
|
||||
<Description>{settings.网站描述 || '展示我的创意作品和项目'}</Description>
|
||||
<Author>{settings.站长 || '树萌芽'}</Author>
|
||||
</HeaderContent>
|
||||
</HeaderContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -1,6 +0,0 @@
|
||||
@echo off
|
||||
echo 启动树萌芽の作品集前端服务...
|
||||
cd frontend
|
||||
npm start
|
||||
pause
|
||||
npm install
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
初始要求.md
41
初始要求.md
@@ -1,41 +0,0 @@
|
||||
开发一个名为 树萌芽の作品集 网站,具体要求如下:
|
||||
|
||||
1. 前端架构:
|
||||
- 采用模块化架构避免单个文件过大,使用React最新版开发 放在frontend目录
|
||||
- 确保代码结构清晰,便于后期扩展维护
|
||||
- 先保证简单展示就可以了
|
||||
|
||||
2. 响应式设计:
|
||||
- 分别编写手机端和电脑端的专用代码
|
||||
- 优先保证手机端用户体验
|
||||
|
||||
3. 后端开发:
|
||||
- 使用Python 3.13.2开发
|
||||
- 后端代码集中存放在独立文件夹backend目录
|
||||
- 先保证能正确读取解析已有的作品,完成最核心的作品下载功能
|
||||
|
||||
4. 后台管理系统:
|
||||
- 开发简洁但功能完整的作品管理界面
|
||||
- 功能包括:
|
||||
* 上传多平台作品文件(Windows/Android/Linux)
|
||||
* 设置作品元数据:名称、版本号、唯一ID
|
||||
* 上传作品图片(可选)并设置首页展示图
|
||||
* 上传作品视频(可选)
|
||||
* 编辑作品信息:作者、标签、分类、介绍
|
||||
|
||||
5. 数据存储:
|
||||
- 创建setting.json存储网站基础配置
|
||||
- 使用work_config.json单独存储每个作品的信息
|
||||
|
||||
6. 前端设计:
|
||||
- 采用淡绿色清新可爱的配色方案
|
||||
- 首页展示作品卡片包含:
|
||||
* 作品名称(标题)
|
||||
* 简短介绍
|
||||
* 标签分类
|
||||
* 上传/更新时间
|
||||
* 支持平台
|
||||
* 版本信息
|
||||
* 作者信息
|
||||
* 作品截图
|
||||
- 实现作品搜索功能@config/ @settings.json @works/ @aicodevartool/ @Windows/ @style.css @主题配色参考/
|
||||
Reference in New Issue
Block a user