Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 173 additions & 42 deletions client/src/components/MediaPreview/MediaPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,122 @@ const MediaPreview = (props: { value: string }) => {
width: 0,
height: 0,
});
const [imageCache, setImageCache] = useState<Map<string, HTMLImageElement>>(
new Map()
);
const [imageError, setImageError] = useState(false);

useEffect(() => {
if (isImageSource(value)) {
setImage(value);

// Create an Image object to get natural dimensions
const img = new Image();
img.src = value;
img.onload = () => {
setImageDimensions({
width: img.naturalWidth,
height: img.naturalHeight,
});
};
setImageError(false); // Reset error state
loadImageWithCache(value);
}
}, [value]);

// Cleanup cache when component unmounts or cache gets too large
useEffect(() => {
const cleanupCache = () => {
if (imageCache.size > 50) {
// Limit cache to 50 images
const entries = Array.from(imageCache.entries());
const newCache = new Map(entries.slice(-30)); // Keep last 30 images
setImageCache(newCache);
}
};

cleanupCache();
}, [imageCache.size]);

// Cleanup on unmount
useEffect(() => {
return () => {
setImageCache(new Map());
};
}, []);

const loadImageWithCache = (imageSrc: string) => {
console.log('Loading image:', imageSrc); // Debug log

// Check if image is already cached
if (imageCache.has(imageSrc)) {
const cachedImg = imageCache.get(imageSrc)!;
setImageDimensions({
width: cachedImg.naturalWidth,
height: cachedImg.naturalHeight,
});
console.log(
'Image loaded from cache:',
cachedImg.naturalWidth,
'x',
cachedImg.naturalHeight
);
return;
}

// Create an Image object to get natural dimensions with caching
const img = new Image();

// Don't set crossOrigin for external images to avoid CORS issues
// The browser will handle CORS automatically if the server allows it

img.onload = () => {
console.log(
'Image loaded successfully:',
img.naturalWidth,
'x',
img.naturalHeight
);
// Cache the loaded image
setImageCache(prev => new Map(prev).set(imageSrc, img));

setImageDimensions({
width: img.naturalWidth,
height: img.naturalHeight,
});
};

img.onerror = () => {
console.warn('Failed to load image:', imageSrc);
setImageError(true);
// Set default dimensions if image fails to load
setImageDimensions({
width: 200,
height: 200,
});
};

// Set src after setting up event handlers
img.src = imageSrc;
};

const handleMouseOver = (e: React.MouseEvent) => {
// Use dynamic image dimensions instead of fixed values
// Use dynamic image dimensions if available, otherwise use defaults
const maxDimension = 200;
const aspectRatio = imageDimensions.width / imageDimensions.height;

let imageWidth, imageHeight;

if (
imageDimensions.width > maxDimension ||
imageDimensions.height > maxDimension
) {
if (aspectRatio > 1) {
// Landscape orientation
imageWidth = maxDimension;
imageHeight = maxDimension / aspectRatio;
let imageWidth = maxDimension;
let imageHeight = maxDimension;

if (imageDimensions.width > 0 && imageDimensions.height > 0) {
const aspectRatio = imageDimensions.width / imageDimensions.height;

if (
imageDimensions.width > maxDimension ||
imageDimensions.height > maxDimension
) {
if (aspectRatio > 1) {
// Landscape orientation
imageWidth = maxDimension;
imageHeight = maxDimension / aspectRatio;
} else {
// Portrait or square orientation
imageHeight = maxDimension;
imageWidth = maxDimension * aspectRatio;
}
} else {
// Portrait or square orientation
imageHeight = maxDimension;
imageWidth = maxDimension * aspectRatio;
// Use original dimensions if they're within the limit
imageWidth = imageDimensions.width;
imageHeight = imageDimensions.height;
}
} else {
// Use original dimensions if they're within the limit
imageWidth = imageDimensions.width;
imageHeight = imageDimensions.height;
}

const offset = 10; // Small offset to position the image beside the cursor
Expand Down Expand Up @@ -82,6 +157,13 @@ const MediaPreview = (props: { value: string }) => {
left: `${left}px`,
zIndex: 1000,
pointerEvents: 'none',
backgroundColor: 'white',
border: '1px solid #ccc',
borderRadius: '4px',
padding: '4px',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
minWidth: '50px',
minHeight: '50px',
});
}
};
Expand All @@ -100,30 +182,75 @@ const MediaPreview = (props: { value: string }) => {
style={isImg ? { cursor: 'pointer' } : {}}
>
{isImg ? (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<icons.img />
<Typography variant="mono" component="p" title={String(value)}>
<a href={value} target="_blank">
<a href={value} target="_blank" rel="noopener noreferrer">
{value}
</a>
</Typography>
</>
</div>
) : (
<Typography variant="mono" component="p" title={String(value)}>
{value}
</Typography>
)}
</div>
{showImage && (
{showImage && image && (
<div style={showImageStyle}>
<img
src={image}
alt="preview"
style={{
width: imageDimensions.width > 200 ? 200 : imageDimensions.width,
borderRadius: '4px',
}}
/>
{imageError ? (
<div
style={{
padding: '20px',
textAlign: 'center',
color: '#666',
fontSize: '12px',
minWidth: '150px',
minHeight: '100px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div>
<div>⚠️ 图片无法加载</div>
<div style={{ marginTop: '8px', fontSize: '11px' }}>
可能是跨域限制
</div>
<div style={{ marginTop: '4px', fontSize: '11px' }}>
点击链接查看原图
</div>
</div>
</div>
) : (
<img
src={image}
alt="preview"
style={{
maxWidth: '200px',
maxHeight: '200px',
width: 'auto',
height: 'auto',
display: 'block',
borderRadius: '2px',
minWidth: '50px',
minHeight: '50px',
}}
onLoad={e => {
console.log(
'Preview image loaded:',
e.currentTarget.naturalWidth,
'x',
e.currentTarget.naturalHeight
);
}}
onError={e => {
console.warn('Failed to display image:', image);
setImageError(true);
e.currentTarget.style.display = 'none';
}}
/>
)}
</div>
)}
</div>
Expand All @@ -132,6 +259,10 @@ const MediaPreview = (props: { value: string }) => {

// Helper function to detect if the value is a URL or Base64-encoded image
function isImageSource(value: string): boolean {
if (!value || typeof value !== 'string') {
return false;
}

const urlPattern = /\.(jpeg|jpg|gif|png|bmp|webp|svg)$/i;
const base64Pattern =
/^data:image\/(png|jpeg|jpg|gif|bmp|webp|svg\+xml);base64,/i;
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/grid/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const EnhancedTable: FC<TableType> = props => {
stickyHeader
sx={{
minWidth: '100%',
height: hasData ? 'auto' : '100%',
height: hasData ? 'auto' : 'fit-content',
}}
aria-labelledby="tableTitle"
size="medium"
Expand Down
2 changes: 2 additions & 0 deletions client/src/components/grid/TableEditableHead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type { TableEditableHeadType } from './Types';

const StyledTableCell = styled(TableCell)(({ theme }) => ({
paddingLeft: theme.spacing(2),
minHeight: 40,
maxHeight: 60,
}));

const StyledTableRow = styled(TableRow)(({ theme }) => ({
Expand Down
1 change: 1 addition & 0 deletions client/src/components/grid/TableHead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const StyledTableHeader = styled(Typography)(({ theme }) => ({
padding: theme.spacing(1.5, 1),
fontWeight: 500,
maxHeight: 45,
minHeight: 20,
fontSize: 13,
overflow: 'hidden',
whiteSpace: 'nowrap',
Expand Down
22 changes: 2 additions & 20 deletions client/src/pages/databases/collections/Collections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ const Collections = () => {
sortType: 'string',
formatter({ collection_name }) {
return (
<Typography variant="body1" sx={{ width: '10vw' }}>
<Box sx={{ maxWidth: '120px' }}>
<Link
to={`/databases/${database}/${collection_name}/overview`}
style={{
Expand All @@ -336,12 +336,9 @@ const Collections = () => {
}}
/>
</Link>
</Typography>
</Box>
);
},
getStyle: () => {
return { minWidth: '10vw' };
},
label: collectionTrans('name'),
},
{
Expand All @@ -362,9 +359,6 @@ const Collections = () => {
</Typography>
);
},
getStyle: () => {
return { minWidth: '130px' };
},
},
{
id: 'rowCount',
Expand All @@ -386,9 +380,6 @@ const Collections = () => {
formatter(v) {
return formatNumber(v.rowCount);
},
getStyle: () => {
return { minWidth: '100px' };
},
},
{
id: 'description',
Expand All @@ -406,9 +397,6 @@ const Collections = () => {
formatter(v) {
return v.description || '--';
},
getStyle: () => {
return { minWidth: '120px' };
},
},
{
id: 'createdTime',
Expand All @@ -418,9 +406,6 @@ const Collections = () => {
formatter(data) {
return new Date(data.createdTime).toLocaleString();
},
getStyle: () => {
return { minWidth: '165px' };
},
},
];

Expand All @@ -444,9 +429,6 @@ const Collections = () => {
formatter(v) {
return <Aliases aliases={v.aliases} collection={v} />;
},
getStyle: () => {
return { minWidth: '100px' };
},
});
}

Expand Down
Loading
Loading