first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View File

@@ -0,0 +1,264 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import {
Box,
TextField,
InputAdornment,
List,
ListItem,
ListItemButton,
ListItemText,
Paper,
Typography,
ClickAwayListener,
Fade,
Avatar,
useTheme,
alpha
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import LaunchIcon from '@mui/icons-material/Launch';
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
import sites from '@/constant/sites.json';
import { useTranslation } from 'react-i18next';
export function DatasetSearchBar() {
const [searchQuery, setSearchQuery] = useState('');
const [showSuggestions, setShowSuggestions] = useState(false);
const [recentSearches, setRecentSearches] = useState([]);
const searchRef = useRef(null);
const suggestionsRef = useRef(null);
const theme = useTheme();
const { t } = useTranslation();
// 从 localStorage 加载最近搜索
useEffect(() => {
const savedSearches = localStorage.getItem('recentDatasetSearches');
if (savedSearches) {
try {
const searches = JSON.parse(savedSearches);
setRecentSearches(searches);
} catch (e) {
console.error('解析最近搜索失败', e);
}
}
}, []);
// 处理搜索输入变化
const handleSearchChange = event => {
setSearchQuery(event.target.value);
if (event.target.value) {
setShowSuggestions(true);
} else {
setShowSuggestions(false);
}
};
// 处理回车搜索
const handleSearchSubmit = event => {
if (event.key === 'Enter' && searchQuery.trim()) {
// 默认使用第一个搜索引擎
if (sites.length > 0) {
handleSuggestionClick(sites[0]);
}
}
};
// 保存最近搜索
const saveRecentSearch = query => {
if (!query.trim()) return;
// 添加到最近搜索并去重
const updatedSearches = [query, ...recentSearches.filter(s => s !== query)].slice(0, 5);
setRecentSearches(updatedSearches);
// 保存到 localStorage
try {
localStorage.setItem('recentDatasetSearches', JSON.stringify(updatedSearches));
} catch (e) {
console.error('保存最近搜索失败', e);
}
};
// 处理点击搜索建议
const handleSuggestionClick = site => {
if (searchQuery.trim()) {
// 根据不同网站处理搜索参数
let searchUrl = site.link;
// 如果链接中不包含问号,则添加搜索参数
if (site.link.includes('huggingface.co')) {
searchUrl = `${site.link}?sort=trending&search=${encodeURIComponent(searchQuery)}`;
} else if (site.link.includes('kaggle.com')) {
searchUrl = `${site.link}?search=${encodeURIComponent(searchQuery)}`;
} else if (site.link.includes('datasetsearch.research.google.com')) {
searchUrl = `${site.link}/search?query=${encodeURIComponent(searchQuery)}&src=0`;
} else if (site.link.includes('paperswithcode.com')) {
searchUrl = `${site.link}?q=${encodeURIComponent(searchQuery)}`;
} else if (site.link.includes('modelscope.cn')) {
searchUrl = `${site.link}?query=${encodeURIComponent(searchQuery)}`;
} else if (site.link.includes('opendatalab.com')) {
searchUrl = `${site.link}?keywords=${encodeURIComponent(searchQuery)}`;
} else if (site.link.includes('tianchi.aliyun.com')) {
searchUrl = `${site.link}?q=${encodeURIComponent(searchQuery)}`;
} else {
// 默认处理方式在URL后添加搜索参数
searchUrl = `${site.link}${site.link.includes('?') ? '&' : '?'}search=${encodeURIComponent(searchQuery)}`;
}
// 保存最近搜索
saveRecentSearch(searchQuery);
window.open(searchUrl, '_blank');
}
setShowSuggestions(false);
};
// 处理点击外部关闭建议
const handleClickAway = event => {
// 确保点击的不是建议框本身
if (suggestionsRef.current && !suggestionsRef.current.contains(event.target)) {
setShowSuggestions(false);
}
};
return (
<ClickAwayListener onClickAway={handleClickAway}>
<Box sx={{ position: 'relative', width: '100%', zIndex: 1300 }} ref={searchRef}>
<TextField
fullWidth
placeholder={t('datasetSquare.searchPlaceholder')}
value={searchQuery}
onChange={handleSearchChange}
onKeyDown={handleSearchSubmit}
onClick={() => searchQuery && setShowSuggestions(true)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon color="primary" />
</InputAdornment>
),
sx: {
height: 56,
borderRadius: 3,
backgroundColor:
theme.palette.mode === 'dark'
? alpha(theme.palette.background.default, 0.6)
: alpha(theme.palette.background.default, 0.8),
backdropFilter: 'blur(8px)',
px: 2,
transition: 'all 0.3s ease',
boxShadow: `0 0 0 1px ${alpha(theme.palette.primary.main, 0.15)}`,
'&.MuiOutlinedInput-root': {
'& fieldset': {
borderColor: 'transparent'
},
'&:hover fieldset': {
borderColor: 'transparent'
},
'&.Mui-focused': {
boxShadow: `0 0 0 2px ${alpha(theme.palette.primary.main, 0.3)}`,
backgroundColor:
theme.palette.mode === 'dark'
? alpha(theme.palette.background.paper, 0.8)
: alpha(theme.palette.common.white, 0.95)
},
'&.Mui-focused fieldset': {
borderColor: 'transparent'
}
}
}
}}
sx={{
mb: 1,
'& .MuiInputBase-input': {
fontSize: '1rem',
fontWeight: 500,
color: theme.palette.text.primary
},
'& .MuiInputBase-input::placeholder': {
color: alpha(theme.palette.text.primary, 0.6),
opacity: 0.7
}
}}
/>
{/* 搜索建议下拉框 - 使用绝对定位确保不被裁剪 */}
{showSuggestions && searchQuery && (
<Box
ref={suggestionsRef}
sx={{
position: 'absolute',
width: '100%',
zIndex: 9999,
top: 'calc(100% + 8px)',
left: 0,
boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
pointerEvents: 'auto' // 确保可以点击
}}
>
<Fade in={showSuggestions}>
<Paper
elevation={6}
sx={{
width: '100%',
maxHeight: 350,
overflow: 'auto',
borderRadius: 2,
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
position: 'relative'
}}
>
<List>
{sites.slice(0, 5).map((site, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => handleSuggestionClick(site)}
sx={{
py: 1.5,
'&:hover': {
bgcolor: alpha(theme.palette.primary.main, 0.05)
}
}}
>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Avatar
sx={{
width: 28,
height: 28,
mr: 1.5,
bgcolor: alpha(theme.palette.primary.main, 0.1),
color: theme.palette.primary.main
}}
>
<TravelExploreIcon fontSize="small" />
</Avatar>
<Typography>
{t('datasetSquare.searchVia')} <strong>{site.name}</strong> Search
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
"{searchQuery}"
</Typography>
<LaunchIcon fontSize="small" color="action" />
</Box>
</Box>
}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Paper>
</Fade>
</Box>
)}
</Box>
</ClickAwayListener>
);
}

View File

@@ -0,0 +1,197 @@
'use client';
import { Card, CardActionArea, CardContent, CardMedia, Typography, Box, Chip, useTheme, alpha } from '@mui/material';
import LaunchIcon from '@mui/icons-material/Launch';
import StorageIcon from '@mui/icons-material/Storage';
import { useTranslation } from 'react-i18next';
export function DatasetSiteCard({ site }) {
const { name, link, description, image, labels } = site;
const theme = useTheme();
// 处理图片路径,如果没有图片则使用默认图片
const imageUrl = image || `/imgs/default-dataset.png`;
const { t } = useTranslation();
// 处理卡片点击
const handleCardClick = () => {
window.open(link, '_blank');
};
return (
<Card
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
transition: 'all 0.3s ease',
borderRadius: 2,
overflow: 'hidden',
boxShadow: theme.palette.mode === 'dark' ? '0 4px 20px rgba(0,0,0,0.3)' : '0 4px 20px rgba(0,0,0,0.1)',
'&:hover': {
transform: 'translateY(-6px)',
boxShadow: theme.palette.mode === 'dark' ? '0 8px 30px rgba(0,0,0,0.4)' : '0 8px 30px rgba(0,0,0,0.15)',
'& .MuiCardMedia-root': {
transform: 'scale(1.05)'
}
}
}}
>
<CardActionArea
onClick={handleCardClick}
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
height: '100%',
'&:hover': {
'& .card-content': {
background:
theme.palette.mode === 'dark'
? alpha(theme.palette.primary.dark, 0.1)
: alpha(theme.palette.primary.light, 0.1)
}
}
}}
>
{/* 网站截图 */}
<Box sx={{ position: 'relative', width: '100%', height: 160, overflow: 'hidden' }}>
<CardMedia
component="img"
height="160"
image={imageUrl}
alt={name}
sx={{
objectFit: 'cover',
bgcolor: theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100',
transition: 'transform 0.5s ease'
}}
/>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: `linear-gradient(to bottom, transparent 70%, ${theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.8)' : 'rgba(0,0,0,0.5)'})`,
zIndex: 1
}}
/>
<Chip
icon={<StorageIcon fontSize="small" />}
label={t('datasetSquare.dataset')}
size="small"
sx={{
position: 'absolute',
top: 10,
right: 10,
zIndex: 2,
backgroundColor: alpha(theme.palette.background.paper, 0.8),
backdropFilter: 'blur(4px)',
border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}`,
'& .MuiChip-icon': {
color: theme.palette.primary.main
}
}}
/>
</Box>
{/* 网站信息 */}
<CardContent
className="card-content"
sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
transition: 'background 0.3s ease',
p: 2.5
}}
>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 1.5 }}>
<Typography
variant="h6"
component="div"
sx={{
fontWeight: 600,
lineHeight: 1.3,
mb: 0.5,
pr: 2, // 留出空间给图标
color: theme.palette.mode === 'dark' ? theme.palette.primary.light : theme.palette.primary.dark
}}
>
{name}
</Typography>
<LaunchIcon
fontSize="small"
sx={{
color: theme.palette.mode === 'dark' ? theme.palette.primary.light : theme.palette.primary.main,
opacity: 0.8,
mt: 0.5
}}
/>
</Box>
<Typography
variant="body2"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
color: theme.palette.text.secondary,
lineHeight: 1.6,
mb: 1
}}
>
{description}
</Typography>
<Box sx={{ mt: 'auto', pt: 1.5 }}>
{/* 标签显示 */}
{labels && labels.length > 0 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mb: 1.5 }}>
{labels.map((label, index) => (
<Chip
key={index}
label={label}
size="small"
sx={{
borderRadius: 1,
height: 20,
fontSize: '0.65rem',
backgroundColor: alpha(theme.palette.primary.main, 0.1),
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: alpha(theme.palette.primary.main, 0.2)
}
}}
/>
))}
</Box>
)}
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Chip
label={t('datasetSquare.viewDataset')}
size="small"
color="primary"
variant="outlined"
sx={{
borderRadius: 1,
height: 24,
fontSize: '0.75rem',
'&:hover': {
backgroundColor: alpha(theme.palette.primary.main, 0.1)
}
}}
/>
</Box>
</Box>
</CardContent>
</CardActionArea>
</Card>
);
}

View File

@@ -0,0 +1,211 @@
'use client';
import { useState, useEffect } from 'react';
import { Grid, Box, Typography, Skeleton, Divider, Tabs, Tab, Fade, Chip, useTheme, alpha, Paper } from '@mui/material';
import StorageIcon from '@mui/icons-material/Storage';
import CategoryIcon from '@mui/icons-material/Category';
import StarIcon from '@mui/icons-material/Star';
import { DatasetSiteCard } from './DatasetSiteCard';
import sites from '@/constant/sites.json';
import { useTranslation } from 'react-i18next';
export function DatasetSiteList() {
const [loading, setLoading] = useState(true);
const theme = useTheme();
const { t } = useTranslation();
// 定义类别
const CATEGORIES = {
ALL: t('datasetSquare.categories.all'),
POPULAR: t('datasetSquare.categories.popular'),
CHINESE: t('datasetSquare.categories.chinese'),
ENGLISH: t('datasetSquare.categories.english'),
RESEARCH: t('datasetSquare.categories.research'),
MULTIMODAL: t('datasetSquare.categories.multimodal')
};
const [activeCategory, setActiveCategory] = useState(CATEGORIES.ALL);
// 模拟加载效果
useEffect(() => {
const timer = setTimeout(() => {
setLoading(false);
}, 800);
return () => clearTimeout(timer);
}, []);
// 处理类别切换
const handleCategoryChange = (event, newValue) => {
setActiveCategory(newValue);
};
// 根据当前选中的类别过滤网站
const getFilteredSites = () => {
if (activeCategory === CATEGORIES.ALL) {
return sites;
} else if (activeCategory === CATEGORIES.POPULAR) {
return sites.filter(site => site.labels && site.labels.includes(t('datasetSquare.categories.popular')));
} else if (activeCategory === CATEGORIES.CHINESE) {
return sites.filter(site => site.labels && site.labels.includes(t('datasetSquare.categories.chinese')));
} else if (activeCategory === CATEGORIES.ENGLISH) {
return sites.filter(site => site.labels && site.labels.includes(t('datasetSquare.categories.english')));
} else if (activeCategory === CATEGORIES.RESEARCH) {
return sites.filter(site => site.labels && site.labels.includes(t('datasetSquare.categories.research')));
} else if (activeCategory === CATEGORIES.MULTIMODAL) {
return sites.filter(site => site.labels && site.labels.includes(t('datasetSquare.categories.multimodal')));
}
return sites;
};
const filteredSites = getFilteredSites();
return (
<Box sx={{ mt: 4 }}>
{/* 类别选择器 */}
<Box sx={{ mb: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<CategoryIcon sx={{ mr: 1, color: 'primary.main' }} />
<Typography variant="h5" component="h2" fontWeight={600}>
{t('datasetSquare.categoryTitle')}
</Typography>
</Box>
<Paper
elevation={0}
sx={{
borderRadius: 2,
p: 0.5,
bgcolor:
theme.palette.mode === 'dark'
? alpha(theme.palette.primary.dark, 0.1)
: alpha(theme.palette.primary.light, 0.1),
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
overflow: 'auto'
}}
>
<Tabs
value={activeCategory}
onChange={handleCategoryChange}
variant="scrollable"
scrollButtons="auto"
sx={{
minHeight: 48,
'& .MuiTabs-indicator': {
display: 'none'
},
'& .MuiTab-root': {
minHeight: 40,
borderRadius: 2,
mx: 0.5,
transition: 'all 0.2s',
'&.Mui-selected': {
bgcolor:
theme.palette.mode === 'dark' ? alpha(theme.palette.primary.main, 0.2) : theme.palette.primary.main,
color: theme.palette.mode === 'dark' ? theme.palette.primary.light : 'white',
fontWeight: 600
}
}
}}
>
<Tab
value={CATEGORIES.ALL}
label={CATEGORIES.ALL}
icon={<StorageIcon fontSize="small" />}
iconPosition="start"
/>
<Tab
value={CATEGORIES.POPULAR}
label={CATEGORIES.POPULAR}
icon={<StarIcon fontSize="small" />}
iconPosition="start"
/>
<Tab value={CATEGORIES.CHINESE} label={CATEGORIES.CHINESE} />
<Tab value={CATEGORIES.ENGLISH} label={CATEGORIES.ENGLISH} />
<Tab value={CATEGORIES.RESEARCH} label={CATEGORIES.RESEARCH} />
<Tab value={CATEGORIES.MULTIMODAL} label={CATEGORIES.MULTIMODAL} />
</Tabs>
</Paper>
</Box>
{/* 数据集网站列表 */}
<Box sx={{ position: 'relative', minHeight: 300 }}>
{loading ? (
// 加载骨架屏
<Grid container spacing={3}>
{Array.from(new Array(8)).map((_, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={index}>
<Box sx={{ width: '100%', height: '100%' }}>
<Skeleton variant="rectangular" height={160} sx={{ borderRadius: 2 }} />
<Box sx={{ pt: 1.5, px: 0.5 }}>
<Skeleton width="80%" height={28} />
<Skeleton width="100%" />
<Skeleton width="100%" />
<Skeleton width="60%" />
</Box>
</Box>
</Grid>
))}
</Grid>
) : (
<Fade in={!loading} timeout={500}>
<Box>
{/* 结果数量提示 */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle1" color="text.secondary">
{t('datasetSquare.foundResources', { count: filteredSites.length })}{' '}
<Chip
label={filteredSites.length}
size="small"
color="primary"
sx={{ mx: 0.5, height: 20, fontSize: '0.75rem' }}
/>
</Typography>
{activeCategory !== CATEGORIES.ALL && (
<Chip
label={t('datasetSquare.currentFilter', { category: activeCategory })}
size="small"
onDelete={() => setActiveCategory(CATEGORIES.ALL)}
sx={{ borderRadius: 1.5 }}
/>
)}
</Box>
{filteredSites.length > 0 ? (
<Grid container spacing={3}>
{filteredSites.map((site, index) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={index}>
<DatasetSiteCard site={site} />
</Grid>
))}
</Grid>
) : (
<Box
sx={{
py: 10,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
bgcolor:
theme.palette.mode === 'dark'
? alpha(theme.palette.background.paper, 0.2)
: alpha(theme.palette.grey[100], 0.5),
borderRadius: 2
}}
>
<StorageIcon sx={{ fontSize: 60, color: 'text.disabled', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
{t('datasetSquare.noDatasets')}
</Typography>
<Typography variant="body2" color="text.disabled">
{t('datasetSquare.tryOtherCategories')}
</Typography>
</Box>
)}
</Box>
</Fade>
)}
</Box>
</Box>
);
}