feat: resource list can display the cover of the resource
This commit is contained in:
65
src/components/ImageModal.css
Normal file
65
src/components/ImageModal.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/* PC端图片弹窗样式 */
|
||||||
|
.desktop-image-modal .ant-modal-wrap {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-image-modal .ant-modal {
|
||||||
|
top: auto !important;
|
||||||
|
left: auto !important;
|
||||||
|
transform: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-image-modal .ant-modal-content {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端图片弹窗样式 */
|
||||||
|
.mobile-image-modal .ant-modal-wrap {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
padding: 64px 0 0 0 !important; /* 顶部留出标题栏空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-image-modal .ant-modal {
|
||||||
|
top: auto !important;
|
||||||
|
left: auto !important;
|
||||||
|
transform: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
position: relative !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-image-modal .ant-modal-content {
|
||||||
|
border-radius: 0;
|
||||||
|
width: 100% !important;
|
||||||
|
max-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-image-modal .ant-modal-body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保图片容器不会溢出 */
|
||||||
|
.image-modal-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-modal-container img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
188
src/components/ImageModal.tsx
Normal file
188
src/components/ImageModal.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Modal, Spin } from 'antd';
|
||||||
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
|
import './ImageModal.css';
|
||||||
|
|
||||||
|
interface ImageModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
imageUrl: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片缓存
|
||||||
|
const imageCache = new Set<string>();
|
||||||
|
|
||||||
|
const ImageModal: React.FC<ImageModalProps> = ({ visible, imageUrl, onClose }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null);
|
||||||
|
|
||||||
|
// 检测是否为移动端
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMobile = () => {
|
||||||
|
setIsMobile(window.innerWidth <= 768);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkMobile();
|
||||||
|
window.addEventListener('resize', checkMobile);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', checkMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 预加载图片
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && imageUrl) {
|
||||||
|
// 如果图片已缓存,直接显示
|
||||||
|
if (imageCache.has(imageUrl)) {
|
||||||
|
setImageLoaded(true);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setImageLoaded(false);
|
||||||
|
setImageError(false);
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
imageCache.add(imageUrl);
|
||||||
|
setImageDimensions({ width: img.naturalWidth, height: img.naturalHeight });
|
||||||
|
setImageLoaded(true);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
setImageError(true);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
img.src = imageUrl;
|
||||||
|
}
|
||||||
|
}, [visible, imageUrl]);
|
||||||
|
|
||||||
|
// 重置状态当弹窗关闭时
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
setLoading(false);
|
||||||
|
setImageLoaded(false);
|
||||||
|
setImageError(false);
|
||||||
|
setImageDimensions(null);
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
// 计算移动端弹窗高度
|
||||||
|
const getMobileModalHeight = () => {
|
||||||
|
if (imageLoaded && imageDimensions) {
|
||||||
|
// 如果图片已加载,根据图片比例自适应高度
|
||||||
|
const availableHeight = window.innerHeight - 64; // 减去标题栏高度
|
||||||
|
const availableWidth = window.innerWidth;
|
||||||
|
|
||||||
|
// 计算图片按宽度100%显示时的高度
|
||||||
|
const aspectRatio = imageDimensions.height / imageDimensions.width;
|
||||||
|
const calculatedHeight = availableWidth * aspectRatio;
|
||||||
|
|
||||||
|
// 确保高度不超过可用空间的90%
|
||||||
|
const maxHeight = availableHeight * 0.9;
|
||||||
|
const finalHeight = Math.min(calculatedHeight, maxHeight);
|
||||||
|
|
||||||
|
return `${finalHeight}px`;
|
||||||
|
}
|
||||||
|
// 图片未加载时,使用默认高度(除标题栏外的33%)
|
||||||
|
return 'calc((100vh - 64px) * 0.33)';
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalStyle = isMobile ? {
|
||||||
|
// 移动端居中显示,不设置top
|
||||||
|
paddingBottom: 0,
|
||||||
|
margin: 0,
|
||||||
|
} : {
|
||||||
|
// PC端不设置top,让centered属性处理居中
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalBodyStyle = isMobile ? {
|
||||||
|
padding: 0,
|
||||||
|
height: getMobileModalHeight(),
|
||||||
|
minHeight: 'calc((100vh - 64px) * 0.33)', // 最小高度为33%
|
||||||
|
maxHeight: 'calc(100vh - 64px)', // 最大高度不超过可视区域
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
} : {
|
||||||
|
padding: 0,
|
||||||
|
height: '66vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={visible}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={null}
|
||||||
|
closable={false}
|
||||||
|
width={isMobile ? '100vw' : '66vw'}
|
||||||
|
style={modalStyle}
|
||||||
|
bodyStyle={modalBodyStyle}
|
||||||
|
centered={true} // 移动端和PC端都居中显示
|
||||||
|
destroyOnClose
|
||||||
|
maskStyle={{ backgroundColor: 'rgba(0, 0, 0, 0.8)' }}
|
||||||
|
wrapClassName={isMobile ? 'mobile-image-modal' : 'desktop-image-modal'}
|
||||||
|
>
|
||||||
|
{/* 自定义关闭按钮 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
zIndex: 1000,
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 20,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
}}
|
||||||
|
onClick={onClose}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 图片内容 */}
|
||||||
|
<div className="image-modal-container">
|
||||||
|
{loading && (
|
||||||
|
<Spin size="large" style={{ color: '#fff' }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{imageError && (
|
||||||
|
<div style={{ color: '#fff', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: 48, marginBottom: 16 }}>📷</div>
|
||||||
|
<div>图片加载失败</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{imageLoaded && !loading && !imageError && (
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt="预览图片"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageModal;
|
||||||
@@ -3,9 +3,10 @@ import { Layout, Typography, Table, Grid, InputNumber, Button, Space, Tag, messa
|
|||||||
import type { ColumnsType, ColumnType } from 'antd/es/table';
|
import type { ColumnsType, ColumnType } from 'antd/es/table';
|
||||||
import type { FilterDropdownProps } from 'antd/es/table/interface';
|
import type { FilterDropdownProps } from 'antd/es/table/interface';
|
||||||
import type { TableProps } from 'antd';
|
import type { TableProps } from 'antd';
|
||||||
import { SearchOutlined, EllipsisOutlined, DeleteOutlined, ManOutlined, WomanOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
import { SearchOutlined, EllipsisOutlined, DeleteOutlined, ManOutlined, WomanOutlined, ExclamationCircleOutlined, PictureOutlined } from '@ant-design/icons';
|
||||||
import './MainContent.css';
|
import './MainContent.css';
|
||||||
import InputDrawer from './InputDrawer.tsx';
|
import InputDrawer from './InputDrawer.tsx';
|
||||||
|
import ImageModal from './ImageModal.tsx';
|
||||||
import { getPeoples } from '../apis';
|
import { getPeoples } from '../apis';
|
||||||
import type { People } from '../apis';
|
import type { People } from '../apis';
|
||||||
import { deletePeople } from '../apis/people';
|
import { deletePeople } from '../apis/people';
|
||||||
@@ -28,6 +29,7 @@ function transformPeoples(list: People[] = []): Resource[] {
|
|||||||
marital_status: person.marital_status,
|
marital_status: person.marital_status,
|
||||||
introduction: person.introduction || {},
|
introduction: person.introduction || {},
|
||||||
contact: person.contact || '',
|
contact: person.contact || '',
|
||||||
|
cover: person.cover || '',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,6 +518,10 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
|
|||||||
// const [inputResult, setInputResult] = React.useState<any>(null);
|
// const [inputResult, setInputResult] = React.useState<any>(null);
|
||||||
const [swipedRowId, setSwipedRowId] = React.useState<string | null>(null);
|
const [swipedRowId, setSwipedRowId] = React.useState<string | null>(null);
|
||||||
const touchStartRef = React.useRef<{ x: number; y: number } | null>(null);
|
const touchStartRef = React.useRef<{ x: number; y: number } | null>(null);
|
||||||
|
|
||||||
|
// 图片弹窗状态
|
||||||
|
const [imageModalVisible, setImageModalVisible] = React.useState(false);
|
||||||
|
const [currentImageUrl, setCurrentImageUrl] = React.useState('');
|
||||||
|
|
||||||
const handleTableChange: TableProps<Resource>['onChange'] = (pg) => {
|
const handleTableChange: TableProps<Resource>['onChange'] = (pg) => {
|
||||||
setPagination({ current: pg?.current ?? 1, pageSize: pg?.pageSize ?? 10 });
|
setPagination({ current: pg?.current ?? 1, pageSize: pg?.pageSize ?? 10 });
|
||||||
@@ -589,17 +595,44 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
|
|||||||
},
|
},
|
||||||
onFilter: (filterValue: React.Key | boolean, record: Resource) => String(record.name).includes(String(filterValue)),
|
onFilter: (filterValue: React.Key | boolean, record: Resource) => String(record.name).includes(String(filterValue)),
|
||||||
render: (text: string, record: Resource) => {
|
render: (text: string, record: Resource) => {
|
||||||
if (!isMobile) return <span style={{ fontWeight: 600 }}>{text}</span>;
|
// 图片图标逻辑
|
||||||
|
const hasCover = record.cover && record.cover.trim() !== '';
|
||||||
|
const pictureIcon = (
|
||||||
|
<PictureOutlined
|
||||||
|
style={{
|
||||||
|
color: hasCover ? '#1677ff' : '#9ca3af',
|
||||||
|
cursor: hasCover ? 'pointer' : 'default'
|
||||||
|
}}
|
||||||
|
onClick={hasCover ? () => {
|
||||||
|
setCurrentImageUrl(record.cover);
|
||||||
|
setImageModalVisible(true);
|
||||||
|
} : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isMobile) {
|
||||||
|
// 桌面端:姓名后面跟图片图标
|
||||||
|
return (
|
||||||
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<span style={{ fontWeight: 600 }}>{text}</span>
|
||||||
|
{pictureIcon}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端:姓名 + 性别图标 + 图片图标
|
||||||
const g = record.gender;
|
const g = record.gender;
|
||||||
const icon = g === '男'
|
const genderIcon = g === '男'
|
||||||
? <ManOutlined style={{ color: '#1677ff' }} />
|
? <ManOutlined style={{ color: '#1677ff' }} />
|
||||||
: g === '女'
|
: g === '女'
|
||||||
? <WomanOutlined style={{ color: '#eb2f96' }} />
|
? <WomanOutlined style={{ color: '#eb2f96' }} />
|
||||||
: <ExclamationCircleOutlined style={{ color: '#9ca3af' }} />;
|
: <ExclamationCircleOutlined style={{ color: '#9ca3af' }} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
||||||
<span style={{ fontWeight: 600 }}>{text}</span>
|
<span style={{ fontWeight: 600 }}>{text}</span>
|
||||||
{icon}
|
{genderIcon}
|
||||||
|
{pictureIcon}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -820,6 +853,16 @@ const ResourceList: React.FC<Props> = ({ inputOpen = false, onCloseInput, contai
|
|||||||
showUpload={false}
|
showUpload={false}
|
||||||
mode={'search'}
|
mode={'search'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 图片预览弹窗 */}
|
||||||
|
<ImageModal
|
||||||
|
visible={imageModalVisible}
|
||||||
|
imageUrl={currentImageUrl}
|
||||||
|
onClose={() => {
|
||||||
|
setImageModalVisible(false);
|
||||||
|
setCurrentImageUrl('');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user