整理项目架构

This commit is contained in:
2025-09-15 15:26:59 +08:00
parent 594cc6ac6f
commit c48037930c
41 changed files with 1317 additions and 720 deletions

133
.gitignore vendored
View File

@@ -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/

View File

@@ -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`

View File

@@ -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

View File

@@ -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()

View File

@@ -1,6 +1,5 @@
@echo off
echo 启动树萌芽の作品集后端服务...
cd backend
echo 正在启动树萌芽の作品集后端...
python app.py
pause
python -m pip install -r requirements.txt

View File

@@ -0,0 +1 @@
REACT_APP_API_URL=https://work.api.shumengya.top/api

View File

@@ -1,5 +1,4 @@
@echo off
echo 正在构建树萌芽の作品集网站前端
cd frontend
npm run build
pause

View File

@@ -0,0 +1,13 @@
{
"网站名字": "✨ 树萌芽の作品集 ✨",
"网站描述": "🎨 展示个人制作的一些小创意和小项目,欢迎交流讨论 💬",
"站长": "👨‍💻 by-树萌芽",
"联系邮箱": "3205788256@qq.com",
"主题颜色": "#81c784",
"每页作品数量": 6,
"启用搜索": true,
"启用分类": true,
"备案号": "📄 蜀ICP备2025151694号",
"网站页尾": "🌱 树萌芽の作品集 | Copyright © 2025-2025 smy ✨",
"网站logo": "assets/logo.png"
}

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 994 B

After

Width:  |  Height:  |  Size: 994 B

View File

@@ -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>

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

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

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

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -0,0 +1,5 @@
@echo off
echo 正在启动树萌芽の作品集前端...
npm start
pause
npm install

View 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%;
}
}

View File

@@ -1,13 +0,0 @@
{
"网站名字": "树萌芽の作品集",
"网站描述": "展示个人制作的一些小创意和小项目,欢迎交流讨论",
"站长": "by-树萌芽",
"联系邮箱": "3205788256@qq.com",
"主题颜色": "#81c784",
"每页作品数量": 9,
"启用搜索": true,
"启用分类": true,
"备案号": "蜀ICP备2025151694号",
"网站页尾": "树萌芽の作品集 | Copyright © 2025-2025 smy",
"网站logo": "assets/logo.png"
}

View File

@@ -1 +0,0 @@
REACT_APP_API_URL=http://127.0.0.1:5000/api

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
@echo off
echo 启动树萌芽の作品集前端服务...
cd frontend
npm start
pause
npm install

View File

@@ -1,8 +0,0 @@
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}

View File

@@ -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 @主题配色参考/