From c48037930c9e58e4d88ec8e3485b896eb6dbd0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=91=E8=90=8C=E8=8A=BD?= <3205788256@qq.com> Date: Mon, 15 Sep 2025 15:26:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E7=90=86=E9=A1=B9=E7=9B=AE=E6=9E=B6?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 133 +------- API_CONFIG.md | 54 --- ShuMengyaWorks-Web/.gitignore | 181 ---------- {backend => SmyWorkCollect-Backend}/app.py | 117 ++++++- .../requirements.txt | 0 .../start_backend.bat | 3 +- .../后端返回接口.json | 0 SmyWorkCollect-Frontend/.env.local | 1 + .../build_frontend.bat | 1 - SmyWorkCollect-Frontend/config/settings.json | 13 + .../package-lock.json | 0 .../package.json | 0 .../public/.htaccess | 0 .../public/_redirects | 0 .../public/assets/logo.png | Bin .../public/assets/logo.svg | 0 .../public/index.html | 0 .../src/App.js | 90 ++++- .../src/components/AdminPanel.js | 0 .../src/components/CategoryFilter.js | 0 .../src/components/Footer.js | 223 ++++++++++++ .../src/components/Header.js | 242 +++++++++++++ .../src/components/LoadingSpinner.js | 0 .../src/components/Pagination.js | 161 +++++++++ .../src/components/SearchBar.js | 0 .../src/components/UploadProgressModal.js | 0 .../src/components/WorkCard.js | 26 +- .../src/components/WorkDetail.js | 317 +++++++++++++++--- .../src/components/WorkEditor.js | 0 .../src/index.js | 0 .../src/services/adminApi.js | 0 .../src/services/api.js | 0 SmyWorkCollect-Frontend/start_frontend.bat | 5 + SmyWorkCollect-Frontend/test/background.css | 192 +++++++++++ config/settings.json | 13 - frontend/.env.local | 1 - frontend/src/components/Footer.js | 101 ------ frontend/src/components/Header.js | 108 ------ start_frontend.bat | 6 - vercel.json | 8 - 初始要求.md | 41 --- 41 files changed, 1317 insertions(+), 720 deletions(-) delete mode 100644 API_CONFIG.md delete mode 100644 ShuMengyaWorks-Web/.gitignore rename {backend => SmyWorkCollect-Backend}/app.py (84%) rename {backend => SmyWorkCollect-Backend}/requirements.txt (100%) rename start_backend.bat => SmyWorkCollect-Backend/start_backend.bat (54%) rename {backend => SmyWorkCollect-Backend}/后端返回接口.json (100%) create mode 100644 SmyWorkCollect-Frontend/.env.local rename build_frontend.bat => SmyWorkCollect-Frontend/build_frontend.bat (86%) create mode 100644 SmyWorkCollect-Frontend/config/settings.json rename {frontend => SmyWorkCollect-Frontend}/package-lock.json (100%) rename {frontend => SmyWorkCollect-Frontend}/package.json (100%) rename {frontend => SmyWorkCollect-Frontend}/public/.htaccess (100%) rename {frontend => SmyWorkCollect-Frontend}/public/_redirects (100%) rename {frontend => SmyWorkCollect-Frontend}/public/assets/logo.png (100%) rename {frontend => SmyWorkCollect-Frontend}/public/assets/logo.svg (100%) rename {frontend => SmyWorkCollect-Frontend}/public/index.html (100%) rename {frontend => SmyWorkCollect-Frontend}/src/App.js (62%) rename {frontend => SmyWorkCollect-Frontend}/src/components/AdminPanel.js (100%) rename {frontend => SmyWorkCollect-Frontend}/src/components/CategoryFilter.js (100%) create mode 100644 SmyWorkCollect-Frontend/src/components/Footer.js create mode 100644 SmyWorkCollect-Frontend/src/components/Header.js rename {frontend => SmyWorkCollect-Frontend}/src/components/LoadingSpinner.js (100%) create mode 100644 SmyWorkCollect-Frontend/src/components/Pagination.js rename {frontend => SmyWorkCollect-Frontend}/src/components/SearchBar.js (100%) rename {frontend => SmyWorkCollect-Frontend}/src/components/UploadProgressModal.js (100%) rename {frontend => SmyWorkCollect-Frontend}/src/components/WorkCard.js (89%) rename {frontend => SmyWorkCollect-Frontend}/src/components/WorkDetail.js (66%) rename {frontend => SmyWorkCollect-Frontend}/src/components/WorkEditor.js (100%) rename {frontend => SmyWorkCollect-Frontend}/src/index.js (100%) rename {frontend => SmyWorkCollect-Frontend}/src/services/adminApi.js (100%) rename {frontend => SmyWorkCollect-Frontend}/src/services/api.js (100%) create mode 100644 SmyWorkCollect-Frontend/start_frontend.bat create mode 100644 SmyWorkCollect-Frontend/test/background.css delete mode 100644 config/settings.json delete mode 100644 frontend/.env.local delete mode 100644 frontend/src/components/Footer.js delete mode 100644 frontend/src/components/Header.js delete mode 100644 start_frontend.bat delete mode 100644 vercel.json delete mode 100644 初始要求.md diff --git a/.gitignore b/.gitignore index 03eafee..1aa271a 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ \ No newline at end of file +SmyWorkCollect-Frontend/build/ +SmyWorkCollect-Frontend/node_modules/ +SmyWorkCollect-Backend/__pycache__/ +SmyWorkCollect-Backend/works/ \ No newline at end of file diff --git a/API_CONFIG.md b/API_CONFIG.md deleted file mode 100644 index 06b5f41..0000000 --- a/API_CONFIG.md +++ /dev/null @@ -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` diff --git a/ShuMengyaWorks-Web/.gitignore b/ShuMengyaWorks-Web/.gitignore deleted file mode 100644 index dd15ec4..0000000 --- a/ShuMengyaWorks-Web/.gitignore +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/app.py b/SmyWorkCollect-Backend/app.py similarity index 84% rename from backend/app.py rename to SmyWorkCollect-Backend/app.py index 8d7ac06..04fa899 100644 --- a/backend/app.py +++ b/SmyWorkCollect-Backend/app.py @@ -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() diff --git a/backend/requirements.txt b/SmyWorkCollect-Backend/requirements.txt similarity index 100% rename from backend/requirements.txt rename to SmyWorkCollect-Backend/requirements.txt diff --git a/start_backend.bat b/SmyWorkCollect-Backend/start_backend.bat similarity index 54% rename from start_backend.bat rename to SmyWorkCollect-Backend/start_backend.bat index 4ce7b77..1e3d571 100644 --- a/start_backend.bat +++ b/SmyWorkCollect-Backend/start_backend.bat @@ -1,6 +1,5 @@ @echo off -echo 启动树萌芽の作品集后端服务... -cd backend +echo 正在启动树萌芽の作品集后端... python app.py pause python -m pip install -r requirements.txt \ No newline at end of file diff --git a/backend/后端返回接口.json b/SmyWorkCollect-Backend/后端返回接口.json similarity index 100% rename from backend/后端返回接口.json rename to SmyWorkCollect-Backend/后端返回接口.json diff --git a/SmyWorkCollect-Frontend/.env.local b/SmyWorkCollect-Frontend/.env.local new file mode 100644 index 0000000..c852092 --- /dev/null +++ b/SmyWorkCollect-Frontend/.env.local @@ -0,0 +1 @@ +REACT_APP_API_URL=https://work.api.shumengya.top/api \ No newline at end of file diff --git a/build_frontend.bat b/SmyWorkCollect-Frontend/build_frontend.bat similarity index 86% rename from build_frontend.bat rename to SmyWorkCollect-Frontend/build_frontend.bat index 47baf20..485d928 100644 --- a/build_frontend.bat +++ b/SmyWorkCollect-Frontend/build_frontend.bat @@ -1,5 +1,4 @@ @echo off echo 正在构建树萌芽の作品集网站前端 -cd frontend npm run build pause \ No newline at end of file diff --git a/SmyWorkCollect-Frontend/config/settings.json b/SmyWorkCollect-Frontend/config/settings.json new file mode 100644 index 0000000..9814791 --- /dev/null +++ b/SmyWorkCollect-Frontend/config/settings.json @@ -0,0 +1,13 @@ +{ + "网站名字": "✨ 树萌芽の作品集 ✨", + "网站描述": "🎨 展示个人制作的一些小创意和小项目,欢迎交流讨论 💬", + "站长": "👨‍💻 by-树萌芽", + "联系邮箱": "3205788256@qq.com", + "主题颜色": "#81c784", + "每页作品数量": 6, + "启用搜索": true, + "启用分类": true, + "备案号": "📄 蜀ICP备2025151694号", + "网站页尾": "🌱 树萌芽の作品集 | Copyright © 2025-2025 smy ✨", + "网站logo": "assets/logo.png" +} \ No newline at end of file diff --git a/frontend/package-lock.json b/SmyWorkCollect-Frontend/package-lock.json similarity index 100% rename from frontend/package-lock.json rename to SmyWorkCollect-Frontend/package-lock.json diff --git a/frontend/package.json b/SmyWorkCollect-Frontend/package.json similarity index 100% rename from frontend/package.json rename to SmyWorkCollect-Frontend/package.json diff --git a/frontend/public/.htaccess b/SmyWorkCollect-Frontend/public/.htaccess similarity index 100% rename from frontend/public/.htaccess rename to SmyWorkCollect-Frontend/public/.htaccess diff --git a/frontend/public/_redirects b/SmyWorkCollect-Frontend/public/_redirects similarity index 100% rename from frontend/public/_redirects rename to SmyWorkCollect-Frontend/public/_redirects diff --git a/frontend/public/assets/logo.png b/SmyWorkCollect-Frontend/public/assets/logo.png similarity index 100% rename from frontend/public/assets/logo.png rename to SmyWorkCollect-Frontend/public/assets/logo.png diff --git a/frontend/public/assets/logo.svg b/SmyWorkCollect-Frontend/public/assets/logo.svg similarity index 100% rename from frontend/public/assets/logo.svg rename to SmyWorkCollect-Frontend/public/assets/logo.svg diff --git a/frontend/public/index.html b/SmyWorkCollect-Frontend/public/index.html similarity index 100% rename from frontend/public/index.html rename to SmyWorkCollect-Frontend/public/index.html diff --git a/frontend/src/App.js b/SmyWorkCollect-Frontend/src/App.js similarity index 62% rename from frontend/src/App.js rename to SmyWorkCollect-Frontend/src/App.js index 3621bf7..d90fe7a 100644 --- a/frontend/src/App.js +++ b/SmyWorkCollect-Frontend/src/App.js @@ -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 ( @@ -128,14 +189,23 @@ const HomePage = () => { {loading ? ( ) : works.length > 0 ? ( - - {works.map((work) => ( - - ))} - + <> + + {currentWorks.map((work) => ( + + ))} + + + ) : ( - {searchQuery || selectedCategory ? '没有找到匹配的作品' : '暂无作品'} + {searchQuery || selectedCategory ? '🔍 没有找到匹配的作品' : '📝 暂无作品'} )} @@ -163,7 +233,7 @@ function App() {
- } /> + } /> } /> } /> diff --git a/frontend/src/components/AdminPanel.js b/SmyWorkCollect-Frontend/src/components/AdminPanel.js similarity index 100% rename from frontend/src/components/AdminPanel.js rename to SmyWorkCollect-Frontend/src/components/AdminPanel.js diff --git a/frontend/src/components/CategoryFilter.js b/SmyWorkCollect-Frontend/src/components/CategoryFilter.js similarity index 100% rename from frontend/src/components/CategoryFilter.js rename to SmyWorkCollect-Frontend/src/components/CategoryFilter.js diff --git a/SmyWorkCollect-Frontend/src/components/Footer.js b/SmyWorkCollect-Frontend/src/components/Footer.js new file mode 100644 index 0000000..68bf84b --- /dev/null +++ b/SmyWorkCollect-Frontend/src/components/Footer.js @@ -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 ( + + + + + 📧 联系邮箱: + {settings.联系邮箱} + + + + + {settings.备案号 && ( + {settings.备案号} + )} + + + {settings.网站页尾 || '🌱 树萌芽の作品集 | Copyright © 2025 smy ✨'} + + + + ); +}; + +export default Footer; diff --git a/SmyWorkCollect-Frontend/src/components/Header.js b/SmyWorkCollect-Frontend/src/components/Header.js new file mode 100644 index 0000000..fbb4561 --- /dev/null +++ b/SmyWorkCollect-Frontend/src/components/Header.js @@ -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 ( + + + {settings.网站logo && ( + + { + e.target.style.display = 'none'; + }} + /> + + )} + {settings.网站名字 || '树萌芽の作品集'} + {settings.网站描述 || '展示我的创意作品和项目'} + {settings.站长 || '树萌芽'} + + + ); +}; + +export default Header; diff --git a/frontend/src/components/LoadingSpinner.js b/SmyWorkCollect-Frontend/src/components/LoadingSpinner.js similarity index 100% rename from frontend/src/components/LoadingSpinner.js rename to SmyWorkCollect-Frontend/src/components/LoadingSpinner.js diff --git a/SmyWorkCollect-Frontend/src/components/Pagination.js b/SmyWorkCollect-Frontend/src/components/Pagination.js new file mode 100644 index 0000000..bd76c4d --- /dev/null +++ b/SmyWorkCollect-Frontend/src/components/Pagination.js @@ -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 ( + + {/* 上一页按钮 */} + handlePageClick(currentPage - 1)} + disabled={currentPage === 1} + > + ← 上一页 + + + {/* 页码按钮 */} + {getPageNumbers().map((page, index) => ( + page === '...' ? ( + ... + ) : ( + handlePageClick(page)} + > + {page} + + ) + ))} + + {/* 下一页按钮 */} + handlePageClick(currentPage + 1)} + disabled={currentPage === totalPages} + > + 下一页 → + + + {/* 页面信息 */} + + 第 {startItem}-{endItem} 项,共 {totalItems} 项 + + + ); +}; + +export default Pagination; diff --git a/frontend/src/components/SearchBar.js b/SmyWorkCollect-Frontend/src/components/SearchBar.js similarity index 100% rename from frontend/src/components/SearchBar.js rename to SmyWorkCollect-Frontend/src/components/SearchBar.js diff --git a/frontend/src/components/UploadProgressModal.js b/SmyWorkCollect-Frontend/src/components/UploadProgressModal.js similarity index 100% rename from frontend/src/components/UploadProgressModal.js rename to SmyWorkCollect-Frontend/src/components/UploadProgressModal.js diff --git a/frontend/src/components/WorkCard.js b/SmyWorkCollect-Frontend/src/components/WorkCard.js similarity index 89% rename from frontend/src/components/WorkCard.js rename to SmyWorkCollect-Frontend/src/components/WorkCard.js index 5079f11..777ccdc 100644 --- a/frontend/src/components/WorkCard.js +++ b/SmyWorkCollect-Frontend/src/components/WorkCard.js @@ -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} - 🎮 + 🎨 @@ -212,13 +214,13 @@ const WorkCard = ({ work }) => { )} - 作者: {work.作者} - v{work.作品版本号} + 👨‍💻 作者: {work.作者} + 🏷️ v{work.作品版本号} - 分类: {work.作品分类} - {formatDate(work.更新时间)} + 📂 分类: {work.作品分类} + 📅 {formatDate(work.更新时间)} {work.支持平台 && work.支持平台.length > 0 && ( @@ -231,25 +233,25 @@ const WorkCard = ({ work }) => { - 浏览量 + 👀 {work.作品浏览量 || 0} - 下载量 + 📥 {work.作品下载量 || 0} - 点赞数 + 💖 {work.作品点赞量 || 0} - 更新次数 + 🔄 {work.作品更新次数 || 0} - 点击查看详情 → + 🌟 点击查看详情 → diff --git a/frontend/src/components/WorkDetail.js b/SmyWorkCollect-Frontend/src/components/WorkDetail.js similarity index 66% rename from frontend/src/components/WorkDetail.js rename to SmyWorkCollect-Frontend/src/components/WorkDetail.js index 87f20a1..f6e36a6 100644 --- a/frontend/src/components/WorkDetail.js +++ b/SmyWorkCollect-Frontend/src/components/WorkDetail.js @@ -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 ( navigate('/')}> - ← 返回首页 + 返回首页 {error} @@ -458,7 +655,7 @@ const WorkDetail = () => { return ( navigate('/')}> - ← 返回首页 + 返回首页 作品不存在 @@ -476,19 +673,19 @@ const WorkDetail = () => { - 作者: {work.作者} + 👨‍💻 作者: {work.作者} - 版本: {work.作品版本号} + 🏷️ 版本: {work.作品版本号} - 分类: {work.作品分类} + 📂 分类: {work.作品分类} - 上传时间: {formatDate(work.上传时间)} + 📅 上传时间: {formatDate(work.上传时间)} - 更新时间: {formatDate(work.更新时间)} + 🔄 更新时间: {formatDate(work.更新时间)} @@ -513,17 +710,17 @@ const WorkDetail = () => { {/* 统计数据 */} - 👁️ + 👁️‍🗨️ {work.作品浏览量 || 0} 浏览量 - ⬇️ + 📥 {work.作品下载量 || 0} 下载量 - 👍 + 💖 {work.作品点赞量 || 0} 点赞量 @@ -544,8 +741,8 @@ const WorkDetail = () => { position: 'relative' }} > - 👍 - {liking ? '点赞中...' : '点赞作品'} + 💖 + {liking ? '💫 点赞中...' : '点赞作品'} {likeMessage && (
{ {work.视频链接 && work.视频链接.length > 0 && ( - 作品视频 + 🎬 作品视频 {work.视频链接.map((videoUrl, index) => ( - + handleVideoClick(videoUrl, index)} + style={{ cursor: 'pointer' }} + > 您的浏览器不支持视频播放 @@ -579,28 +780,9 @@ const WorkDetail = () => { )} - {work.图片链接 && work.图片链接.length > 0 && ( - - 作品截图 - - {work.图片链接.map((imageUrl, index) => ( - handleImageClick(imageUrl)} - onError={(e) => { - e.target.style.display = 'none'; - }} - /> - ))} - - - )} - {work.下载链接 && Object.keys(work.下载链接).length > 0 && ( - 下载作品 + 📦 下载作品 {Object.entries(work.下载链接).map(([platform, links]) => ( @@ -611,7 +793,7 @@ const WorkDetail = () => { href={`${getApiBaseUrl()}${link}`} download > - 下载 {platform} 版本 + 📥 下载 {platform} 版本 ))} @@ -619,6 +801,53 @@ const WorkDetail = () => { )} + + {work.图片链接 && work.图片链接.length > 0 && ( + + 🖼️ 作品截图 + + {work.图片链接.map((imageUrl, index) => ( + handleImageClick(imageUrl, index)} + onError={(e) => { + e.target.style.display = 'none'; + }} + /> + ))} + + + )} + + {/* 模态框 */} + {modalOpen && ( + + + × + {modalType === 'image' ? ( + { + console.error('图片加载失败:', modalSrc); + }} + /> + ) : modalType === 'video' ? ( + { + console.error('视频加载失败:', modalSrc); + }} + /> + ) : null} + {modalTitle} + + + )} ); }; diff --git a/frontend/src/components/WorkEditor.js b/SmyWorkCollect-Frontend/src/components/WorkEditor.js similarity index 100% rename from frontend/src/components/WorkEditor.js rename to SmyWorkCollect-Frontend/src/components/WorkEditor.js diff --git a/frontend/src/index.js b/SmyWorkCollect-Frontend/src/index.js similarity index 100% rename from frontend/src/index.js rename to SmyWorkCollect-Frontend/src/index.js diff --git a/frontend/src/services/adminApi.js b/SmyWorkCollect-Frontend/src/services/adminApi.js similarity index 100% rename from frontend/src/services/adminApi.js rename to SmyWorkCollect-Frontend/src/services/adminApi.js diff --git a/frontend/src/services/api.js b/SmyWorkCollect-Frontend/src/services/api.js similarity index 100% rename from frontend/src/services/api.js rename to SmyWorkCollect-Frontend/src/services/api.js diff --git a/SmyWorkCollect-Frontend/start_frontend.bat b/SmyWorkCollect-Frontend/start_frontend.bat new file mode 100644 index 0000000..cf8b2d9 --- /dev/null +++ b/SmyWorkCollect-Frontend/start_frontend.bat @@ -0,0 +1,5 @@ +@echo off +echo 正在启动树萌芽の作品集前端... +npm start +pause +npm install diff --git a/SmyWorkCollect-Frontend/test/background.css b/SmyWorkCollect-Frontend/test/background.css new file mode 100644 index 0000000..04fcd85 --- /dev/null +++ b/SmyWorkCollect-Frontend/test/background.css @@ -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%; + } +} \ No newline at end of file diff --git a/config/settings.json b/config/settings.json deleted file mode 100644 index 25a1339..0000000 --- a/config/settings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "网站名字": "树萌芽の作品集", - "网站描述": "展示个人制作的一些小创意和小项目,欢迎交流讨论", - "站长": "by-树萌芽", - "联系邮箱": "3205788256@qq.com", - "主题颜色": "#81c784", - "每页作品数量": 9, - "启用搜索": true, - "启用分类": true, - "备案号": "蜀ICP备2025151694号", - "网站页尾": "树萌芽の作品集 | Copyright © 2025-2025 smy", - "网站logo": "assets/logo.png" -} \ No newline at end of file diff --git a/frontend/.env.local b/frontend/.env.local deleted file mode 100644 index a76018d..0000000 --- a/frontend/.env.local +++ /dev/null @@ -1 +0,0 @@ -REACT_APP_API_URL=http://127.0.0.1:5000/api \ No newline at end of file diff --git a/frontend/src/components/Footer.js b/frontend/src/components/Footer.js deleted file mode 100644 index 9e18060..0000000 --- a/frontend/src/components/Footer.js +++ /dev/null @@ -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 ( - - - - - 联系邮箱: - {settings.联系邮箱} - - - - - {settings.备案号 && ( - {settings.备案号} - )} - - - {settings.网站页尾 || '树萌芽の作品集 | Copyright © 2025 smy'} - - - - ); -}; - -export default Footer; diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js deleted file mode 100644 index 8a9f569..0000000 --- a/frontend/src/components/Header.js +++ /dev/null @@ -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 ( - - - {settings.网站logo && ( - - { - e.target.style.display = 'none'; - }} - /> - - )} - {settings.网站名字 || '树萌芽の作品集'} - {settings.网站描述 || '展示我的创意作品和项目'} - {settings.站长 || '树萌芽'} - - - ); -}; - -export default Header; diff --git a/start_frontend.bat b/start_frontend.bat deleted file mode 100644 index c7b13d5..0000000 --- a/start_frontend.bat +++ /dev/null @@ -1,6 +0,0 @@ -@echo off -echo 启动树萌芽の作品集前端服务... -cd frontend -npm start -pause -npm install diff --git a/vercel.json b/vercel.json deleted file mode 100644 index 1323cda..0000000 --- a/vercel.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "rewrites": [ - { - "source": "/(.*)", - "destination": "/index.html" - } - ] -} diff --git a/初始要求.md b/初始要求.md deleted file mode 100644 index 284ab29..0000000 --- a/初始要求.md +++ /dev/null @@ -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 @主题配色参考/ \ No newline at end of file