Files
SmyWorkCollect/SproutWorkCollect-Frontend/src/App.js
2026-03-18 22:09:43 +08:00

314 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;