feat(imageGrid): enhance layout and responsiveness of image grid and cards

This commit is contained in:
shiyu
2025-06-06 14:33:06 +08:00
parent a95651b04a
commit 40106095aa
3 changed files with 141 additions and 66 deletions
+102 -54
View File
@@ -1,27 +1,36 @@
.image-grid { .image-grid {
margin-bottom: 40px; margin-bottom: 40px;
display: grid; display: flex;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); flex-wrap: wrap;
grid-gap: 24px; gap: 3px;
justify-content: space-between;
/* 改为space-between让每行铺满 */
align-items: flex-start;
}
.image-grid::after {
content: '';
flex: auto;
} }
/* 现代化卡片样式 */
.custom-card { .custom-card {
position: relative; position: relative;
border-radius: 12px;
overflow: hidden; overflow: hidden;
box-shadow: 0 8px 20px rgba(0,0,0,0.08); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transition: all 0.35s cubic-bezier(0.23, 1, 0.32, 1); transition: all 0.35s cubic-bezier(0.23, 1, 0.32, 1);
height: 100%;
background: #ffffff; background: #ffffff;
transform: translateY(0); transform: translateY(0);
cursor: pointer; cursor: pointer;
aspect-ratio: 1 / 1; height: 250px;
flex-grow: 1;
flex-shrink: 0;
min-width: 180px;
max-width: 400px;
} }
.custom-card:hover { .custom-card:hover {
transform: translateY(-5px); transform: translateY(-3px);
box-shadow: 0 14px 28px rgba(0,0,0,0.15); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
} }
/* 图片占满卡片 */ /* 图片占满卡片 */
@@ -51,13 +60,13 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0) 50%); background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0) 50%);
opacity: 0; opacity: 0;
transition: opacity 0.35s ease; transition: opacity 0.35s ease;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
padding: 20px; padding: 16px;
color: white; color: white;
} }
@@ -78,44 +87,46 @@
} }
.custom-card-title { .custom-card-title {
font-size: 18px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #ffffff; color: #ffffff;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-bottom: 8px; margin-bottom: 6px;
text-shadow: 0 1px 3px rgba(0,0,0,0.3); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
} }
.custom-card-tags-container { .custom-card-tags-container {
margin-top: 6px; margin-top: 4px;
margin-bottom: 10px; margin-bottom: 8px;
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
display: flex;
flex-wrap: wrap;
gap: 4px;
} }
.image-tag { .image-tag {
margin-right: 6px; font-size: 10px !important;
font-size: 11px !important; background: rgba(255, 255, 255, 0.2);
background: rgba(255,255,255,0.2); padding: 2px 6px;
padding: 3px 8px; border-radius: 3px;
border-radius: 4px;
color: #ffffff; color: #ffffff;
display: inline-block; display: inline-block;
margin-bottom: 4px;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
white-space: nowrap;
} }
/* 权限和元数据指示器 */ /* 权限和元数据指示器 */
.custom-card-indicators { .custom-card-indicators {
position: absolute; position: absolute;
top: 12px; top: 8px;
left: 0; left: 0;
right: 0; right: 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 0 12px; padding: 0 8px;
opacity: 0; opacity: 0;
transition: opacity 0.35s ease; transition: opacity 0.35s ease;
z-index: 2; z-index: 2;
@@ -128,25 +139,25 @@
.custom-card-permission { .custom-card-permission {
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.6);
color: white; color: white;
border-radius: 20px; border-radius: 12px;
padding: 5px 10px; padding: 3px 8px;
font-size: 12px; font-size: 10px;
font-weight: 500; font-weight: 500;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 3px;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
box-shadow: 0 2px 8px rgba(0,0,0,0.2); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
} }
.custom-card-metadata { .custom-card-metadata {
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.6);
color: white; color: white;
border-radius: 20px; border-radius: 12px;
padding: 5px 10px; padding: 3px 8px;
font-size: 12px; font-size: 10px;
font-weight: 500; font-weight: 500;
box-shadow: 0 2px 8px rgba(0,0,0,0.2); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }
@@ -154,20 +165,22 @@
.custom-card-actions { .custom-card-actions {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-top: 12px; margin-top: 8px;
gap: 6px;
} }
.custom-card-action-item { .custom-card-action-item {
background-color: rgba(255, 255, 255, 0.15); background-color: rgba(255, 255, 255, 0.15);
border-radius: 50%; border-radius: 50%;
width: 36px; width: 28px;
height: 36px; height: 28px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
flex-shrink: 0;
} }
.custom-card-action-item:hover { .custom-card-action-item:hover {
@@ -179,7 +192,7 @@
.context-menu { .context-menu {
background-color: white; background-color: white;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
padding: 8px 0; padding: 8px 0;
min-width: 160px; min-width: 160px;
z-index: 1000; z-index: 1000;
@@ -200,38 +213,38 @@
/* 选中状态样式 */ /* 选中状态样式 */
.custom-card-selected { .custom-card-selected {
box-shadow: 0 0 0 3px #1890ff, 0 14px 28px rgba(0,0,0,0.15) !important; box-shadow: 0 0 0 3px #1890ff, 0 8px 20px rgba(0, 0, 0, 0.15) !important;
} }
.custom-card-selected::before { .custom-card-selected::before {
content: ''; content: '';
position: absolute; position: absolute;
top: 10px; top: 8px;
right: 10px; right: 8px;
width: 22px; width: 20px;
height: 22px; height: 20px;
border-radius: 50%; border-radius: 50%;
background-color: #1890ff; background-color: #1890ff;
z-index: 5; z-index: 5;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.2); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
} }
.custom-card-selected::after { .custom-card-selected::after {
content: '✓'; content: '✓';
position: absolute; position: absolute;
top: 10px; top: 8px;
right: 10px; right: 8px;
width: 22px; width: 20px;
height: 22px; height: 20px;
z-index: 6; z-index: 6;
color: white; color: white;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 14px; font-size: 12px;
} }
.image-grid-pagination { .image-grid-pagination {
@@ -252,19 +265,54 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: loading 1.5s infinite; animation: loading 1.5s infinite;
} }
@keyframes loading { @keyframes loading {
0% { transform: translateX(-100%); } 0% {
100% { transform: translateX(100%); } transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
} }
/* 响应式调整 */ /* 响应式调整 */
@media (max-width: 768px) { @media (max-width: 768px) {
.image-grid { .image-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px;
grid-gap: 16px; }
.custom-card {
height: 150px;
}
.custom-card-overlay {
padding: 12px;
}
.custom-card-title {
font-size: 12px;
}
.custom-card-actions {
gap: 4px;
}
.custom-card-action-item {
width: 24px;
height: 24px;
}
}
@media (max-width: 480px) {
.custom-card {
height: 120px;
}
.custom-card-overlay {
padding: 8px;
} }
} }
+36 -9
View File
@@ -439,6 +439,21 @@ const ImageGrid: React.FC<ImageGridProps> = ({
return !!user && !!image.userId && user.id === image.userId; return !!user && !!image.userId && user.id === image.userId;
}; };
// 计算图片宽度的函数 - 修改为计算最小宽度
const calculateImageMinWidth = (image: PictureResponse): number => {
const fixedHeight = 200; // 固定高度
const defaultMinWidth = 180; // 默认最小宽度
if (image.exifInfo?.width && image.exifInfo?.height) {
const aspectRatio = image.exifInfo.width / image.exifInfo.height;
const calculatedWidth = Math.round(fixedHeight * aspectRatio);
// 确保最小宽度不小于180px,最大不超过400px
return Math.max(180, Math.min(400, calculatedWidth));
}
return defaultMinWidth;
};
// 优化渲染内容函数 // 优化渲染内容函数
const renderContent = () => { const renderContent = () => {
// 渲染加载状态 // 渲染加载状态
@@ -446,7 +461,11 @@ const ImageGrid: React.FC<ImageGridProps> = ({
return ( return (
<div className="image-grid"> <div className="image-grid">
{Array.from({ length: pageSize }).map((_, index) => ( {Array.from({ length: pageSize }).map((_, index) => (
<div key={`loading-${index}`} className="custom-card image-loading-effect"> <div
key={`loading-${index}`}
className="custom-card image-loading-effect"
style={{ minWidth: 180 }}
>
<div className="custom-card-cover" style={{ background: '#f5f5f5' }}> <div className="custom-card-cover" style={{ background: '#f5f5f5' }}>
{/* 简单的加载状态 */} {/* 简单的加载状态 */}
</div> </div>
@@ -472,11 +491,16 @@ const ImageGrid: React.FC<ImageGridProps> = ({
<div className="image-grid"> <div className="image-grid">
{images.map((image, index) => { {images.map((image, index) => {
const isOwner = canEditImage(image); const isOwner = canEditImage(image);
const imageMinWidth = calculateImageMinWidth(image);
return ( return (
<div <div
key={image.id} key={image.id}
className={`custom-card ${selectedIds.includes(image.id) ? 'custom-card-selected' : ''} ${selectable ? 'custom-card-selectable-mode' : ''}`} className={`custom-card ${selectedIds.includes(image.id) ? 'custom-card-selected' : ''} ${selectable ? 'custom-card-selectable-mode' : ''}`}
style={{
minWidth: imageMinWidth,
flexBasis: imageMinWidth // 设置flex基准值
}}
onClick={() => handleImageClick(image, index)} onClick={() => handleImageClick(image, index)}
onContextMenu={(e) => handleContextMenu(e, image)} onContextMenu={(e) => handleContextMenu(e, image)}
> >
@@ -519,11 +543,14 @@ const ImageGrid: React.FC<ImageGridProps> = ({
<div className="custom-card-info"> <div className="custom-card-info">
<div className="custom-card-title">{image.name}</div> <div className="custom-card-title">{image.name}</div>
{image.tags && ( {image.tags && image.tags.length > 0 && (
<div className="custom-card-tags-container"> <div className="custom-card-tags-container">
{image.tags.map(tag => ( {image.tags.slice(0, 3).map((tag, tagIndex) => (
<Text key={tag} className="image-tag">#{tag}</Text> <Text key={`${image.id}-${tag}-${tagIndex}`} className="image-tag">#{tag}</Text>
))} ))}
{image.tags.length > 3 && (
<Text className="image-tag">+{image.tags.length - 3}</Text>
)}
</div> </div>
)} )}
@@ -536,9 +563,9 @@ const ImageGrid: React.FC<ImageGridProps> = ({
}} }}
> >
{image.isFavorited ? ( {image.isFavorited ? (
<HeartFilled style={{ fontSize: 16, color: '#ff4d4f' }} /> <HeartFilled style={{ fontSize: 14, color: '#ff4d4f' }} />
) : ( ) : (
<HeartOutlined style={{ fontSize: 16, color: '#ffffff' }} /> <HeartOutlined style={{ fontSize: 14, color: '#ffffff' }} />
)} )}
</div> </div>
@@ -554,7 +581,7 @@ const ImageGrid: React.FC<ImageGridProps> = ({
} }
}} }}
> >
<EditOutlined style={{ fontSize: 16, color: '#ffffff' }} /> <EditOutlined style={{ fontSize: 14, color: '#ffffff' }} />
</div> </div>
)} )}
@@ -565,7 +592,7 @@ const ImageGrid: React.FC<ImageGridProps> = ({
onShare?.(image); onShare?.(image);
}} }}
> >
<ShareAltOutlined style={{ fontSize: 16, color: '#ffffff' }} /> <ShareAltOutlined style={{ fontSize: 14, color: '#ffffff' }} />
</div> </div>
<div <div
@@ -575,7 +602,7 @@ const ImageGrid: React.FC<ImageGridProps> = ({
onDownload?.(image); onDownload?.(image);
}} }}
> >
<DownloadOutlined style={{ fontSize: 16, color: '#ffffff' }} /> <DownloadOutlined style={{ fontSize: 14, color: '#ffffff' }} />
</div> </div>
</div> </div>
</div> </div>
+3 -3
View File
@@ -8,7 +8,7 @@ import {
CompassOutlined, CompassOutlined,
DashboardOutlined, DashboardOutlined,
UserOutlined, UserOutlined,
LogoutOutlined FileTextOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import AllImages from '../pages/allImages/Index'; import AllImages from '../pages/allImages/Index';
@@ -171,10 +171,10 @@ const routes: RouteConfig[] = [
title: '图片管理' title: '图片管理'
} }
}, },
{ {
path: 'log', path: 'log',
key: 'admin-log', key: 'admin-log',
icon: <LogoutOutlined />, icon: <FileTextOutlined />,
label: '日志中心', label: '日志中心',
element: <AdminLogManagement />, element: <AdminLogManagement />,
area: 'admin', area: 'admin',