314 lines
8.8 KiB
JavaScript
314 lines
8.8 KiB
JavaScript
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 (
|
||
<MainContent>
|
||
<FilterSection>
|
||
<SearchBar onSearch={handleSearch} />
|
||
<CategoryFilter
|
||
categories={categories}
|
||
selectedCategory={selectedCategory}
|
||
onCategoryChange={handleCategoryChange}
|
||
/>
|
||
</FilterSection>
|
||
|
||
{loading ? (
|
||
<LoadingSpinner />
|
||
) : works.length > 0 ? (
|
||
<>
|
||
<WorksGrid>
|
||
{currentWorks.map((work) => (
|
||
<WorkCard key={work.作品ID} work={work} />
|
||
))}
|
||
</WorksGrid>
|
||
<Pagination
|
||
currentPage={currentPage}
|
||
totalPages={totalPages}
|
||
totalItems={totalWorks}
|
||
itemsPerPage={itemsPerPage}
|
||
onPageChange={handlePageChange}
|
||
/>
|
||
</>
|
||
) : (
|
||
<NoResults>
|
||
{searchQuery || selectedCategory ? '🔍 没有找到匹配的作品' : '📝 暂无作品'}
|
||
</NoResults>
|
||
)}
|
||
</MainContent>
|
||
);
|
||
};
|
||
|
||
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 (
|
||
<Router>
|
||
<AppContainer
|
||
$backgroundUrl={backgroundUrl}
|
||
$blurAmount={blurConfig.enabled ? blurConfig.amount : '0px'}
|
||
$blurOverlayOpacity={blurConfig.enabled ? blurConfig.overlayOpacity : 0}
|
||
>
|
||
<Header settings={settings} />
|
||
<Routes>
|
||
<Route path="/" element={<HomePage settings={settings} />} />
|
||
<Route path="/work/:workId" element={<WorkDetail />} />
|
||
<Route path="/admin" element={<AdminPanel />} />
|
||
</Routes>
|
||
<Footer settings={settings} />
|
||
</AppContainer>
|
||
</Router>
|
||
);
|
||
}
|
||
|
||
export default App;
|