import React, { useState, useEffect, useRef } from 'react';
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
import styled from 'styled-components';
import Header from './components/Header';
import WorkCard from './components/WorkCard';
import WorkDetail from './components/WorkDetail';
import AdminPanel from './components/AdminPanel';
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, getWorkDetail } from './services/api';
import { BACKGROUND_CONFIG, pickBackgroundImage } from './config/background';
const AppContainer = styled.div`
min-height: 100vh;
background: ${({ $backgroundUrl }) =>
$backgroundUrl
? `url(${$backgroundUrl}) center/cover no-repeat fixed`
: `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: ${({ $backgroundUrl }) => ($backgroundUrl ? 'cover' : '400% 400%')};
animation: ${({ $backgroundUrl }) => ($backgroundUrl ? 'none' : 'gentleShift 25s ease infinite')};
position: relative;
&:before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: ${({ $blurOverlayOpacity }) =>
`rgba(255, 255, 255, ${$blurOverlayOpacity})`};
backdrop-filter: ${({ $blurAmount }) => `blur(${$blurAmount})`};
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`
max-width: 1440px;
margin: 0 auto;
padding: 20px;
@media (max-width: 768px) {
padding: 10px;
}
`;
const WorksGrid = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-top: 20px;
@media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 768px) {
grid-template-columns: 1fr;
gap: 15px;
}
`;
const FilterSection = styled.div`
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 20px;
@media (min-width: 768px) {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
`;
const NoResults = styled.div`
text-align: center;
padding: 40px 20px;
color: #666;
font-size: 18px;
`;
// 首页组件
const HomePage = ({ settings }) => {
const [works, setWorks] = useState([]);
const [totalWorks, setTotalWorks] = useState(0);
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const pageSizeInitRef = useRef(false);
// 从设置中获取每页作品数量,默认12(三行四列)
const itemsPerPage = settings['每页作品数量'] || 12;
useEffect(() => {
loadInitialData();
}, []);
useEffect(() => {
if (!pageSizeInitRef.current) {
pageSizeInitRef.current = true;
return;
}
setCurrentPage(1);
performSearch(searchQuery, selectedCategory, 1);
}, [itemsPerPage]);
const fetchWorksByIds = async (ids) => {
if (!Array.isArray(ids) || ids.length === 0) return [];
const results = await Promise.all(
ids.map(async (id) => {
try {
const detail = await getWorkDetail(id);
return detail?.data || null;
} catch (error) {
console.error('加载作品详情失败:', id, error);
return null;
}
})
);
return results.filter(Boolean);
};
const loadInitialData = async () => {
try {
setLoading(true);
const [worksData, categoriesData] = await Promise.all([
getWorks(1, itemsPerPage),
getCategories()
]);
const rawData = worksData.data || [];
const resolvedWorks = Array.isArray(rawData) && typeof rawData[0] === 'string'
? await fetchWorksByIds(rawData)
: rawData;
setWorks(resolvedWorks);
setTotalWorks(worksData.total || 0);
setCategories(categoriesData.data || []);
setCurrentPage(1); // 重置到第一页
} catch (error) {
console.error('加载数据失败:', error);
} finally {
setLoading(false);
}
};
const handleSearch = async (query) => {
setSearchQuery(query);
setCurrentPage(1);
await performSearch(query, selectedCategory, 1);
};
const handleCategoryChange = async (category) => {
setSelectedCategory(category);
setCurrentPage(1);
await performSearch(searchQuery, category, 1);
};
const performSearch = async (query, category, page) => {
try {
setLoading(true);
if (query || category) {
const searchData = await searchWorks(query, category, page, itemsPerPage);
const rawData = searchData.data || [];
const resolvedWorks = Array.isArray(rawData) && typeof rawData[0] === 'string'
? await fetchWorksByIds(rawData)
: rawData;
setWorks(resolvedWorks);
setTotalWorks(searchData.total || 0);
} else {
const worksData = await getWorks(page, itemsPerPage);
const rawData = worksData.data || [];
const resolvedWorks = Array.isArray(rawData) && typeof rawData[0] === 'string'
? await fetchWorksByIds(rawData)
: rawData;
setWorks(resolvedWorks);
setTotalWorks(worksData.total || 0);
}
} catch (error) {
console.error('搜索失败:', error);
} finally {
setLoading(false);
}
};
// 分页相关的计算
const totalPages = Math.ceil(totalWorks / itemsPerPage);
const currentWorks = works;
// 处理页面变化
const handlePageChange = (page) => {
setCurrentPage(page);
performSearch(searchQuery, selectedCategory, page);
// 滚动到顶部
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return (
{loading ? (
) : works.length > 0 ? (
<>
{currentWorks.map((work) => (
))}
>
) : (
{searchQuery || selectedCategory ? '🔍 没有找到匹配的作品' : '📝 暂无作品'}
)}
);
};
function App() {
const [settings, setSettings] = useState({});
const [backgroundUrl, setBackgroundUrl] = useState(null);
const [blurConfig] = useState(BACKGROUND_CONFIG.blur || { enabled: true, amount: '6px', overlayOpacity: 0.35 });
useEffect(() => {
loadSettings();
}, []);
// 将后端 settings.json 中的「网站名字」同步到浏览器标签页标题
useEffect(() => {
if (settings['网站名字']) {
document.title = settings['网站名字'];
}
}, [settings]);
// 页面初始化时,根据设备类型随机选择一张背景图
useEffect(() => {
const isMobile = window.innerWidth <= 768;
const url = pickBackgroundImage(isMobile);
if (url) {
setBackgroundUrl(url);
}
}, []);
const loadSettings = async () => {
try {
const settingsData = await getSettings();
setSettings(settingsData);
} catch (error) {
console.error('加载设置失败:', error);
}
};
return (
} />
} />
} />
);
}
export default App;