feat: add SproutWorkCollect apps

This commit is contained in:
2026-03-13 17:14:37 +08:00
parent 189baa3d59
commit 46afd3149f
54 changed files with 28126 additions and 4 deletions

View File

@@ -0,0 +1,277 @@
import React, { useState, useEffect } 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 } 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 [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);
// 从设置中获取每页作品数量默认12三行四列
const itemsPerPage = settings['每页作品数量'] || 12;
useEffect(() => {
loadInitialData();
}, []);
const loadInitialData = async () => {
try {
setLoading(true);
const [worksData, categoriesData] = await Promise.all([
getWorks(),
getCategories()
]);
const allWorksData = worksData.data || [];
setAllWorks(allWorksData);
setWorks(allWorksData);
setCategories(categoriesData.data || []);
setCurrentPage(1); // 重置到第一页
} catch (error) {
console.error('加载数据失败:', error);
} finally {
setLoading(false);
}
};
const handleSearch = async (query) => {
setSearchQuery(query);
await performSearch(query, selectedCategory);
};
const handleCategoryChange = async (category) => {
setSelectedCategory(category);
await performSearch(searchQuery, category);
};
const performSearch = async (query, category) => {
try {
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 {
setLoading(false);
}
};
// 分页相关的计算
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>
<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={works.length}
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;