diff --git a/client/src/components/MediaPreview/MediaPreview.tsx b/client/src/components/MediaPreview/MediaPreview.tsx index af006e0b..6c6bc00a 100644 --- a/client/src/components/MediaPreview/MediaPreview.tsx +++ b/client/src/components/MediaPreview/MediaPreview.tsx @@ -11,47 +11,122 @@ const MediaPreview = (props: { value: string }) => { width: 0, height: 0, }); + const [imageCache, setImageCache] = useState>( + 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 @@ -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', }); } }; @@ -100,30 +182,75 @@ const MediaPreview = (props: { value: string }) => { style={isImg ? { cursor: 'pointer' } : {}} > {isImg ? ( - <> +
- + {value} - +
) : ( {value} )} - {showImage && ( + {showImage && image && (
- preview 200 ? 200 : imageDimensions.width, - borderRadius: '4px', - }} - /> + {imageError ? ( +
+
+
⚠️ 图片无法加载
+
+ 可能是跨域限制 +
+
+ 点击链接查看原图 +
+
+
+ ) : ( + preview { + 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'; + }} + /> + )}
)} @@ -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; diff --git a/client/src/components/grid/Table.tsx b/client/src/components/grid/Table.tsx index c455fa69..65cc733c 100644 --- a/client/src/components/grid/Table.tsx +++ b/client/src/components/grid/Table.tsx @@ -62,7 +62,7 @@ const EnhancedTable: FC = props => { stickyHeader sx={{ minWidth: '100%', - height: hasData ? 'auto' : '100%', + height: hasData ? 'auto' : 'fit-content', }} aria-labelledby="tableTitle" size="medium" diff --git a/client/src/components/grid/TableEditableHead.tsx b/client/src/components/grid/TableEditableHead.tsx index 0d1f9a03..8673e660 100644 --- a/client/src/components/grid/TableEditableHead.tsx +++ b/client/src/components/grid/TableEditableHead.tsx @@ -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 }) => ({ diff --git a/client/src/components/grid/TableHead.tsx b/client/src/components/grid/TableHead.tsx index ff46185a..b9098b65 100644 --- a/client/src/components/grid/TableHead.tsx +++ b/client/src/components/grid/TableHead.tsx @@ -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', diff --git a/client/src/pages/databases/collections/Collections.tsx b/client/src/pages/databases/collections/Collections.tsx index 8f0b3b5b..518b9312 100644 --- a/client/src/pages/databases/collections/Collections.tsx +++ b/client/src/pages/databases/collections/Collections.tsx @@ -313,7 +313,7 @@ const Collections = () => { sortType: 'string', formatter({ collection_name }) { return ( - + { }} /> - + ); }, - getStyle: () => { - return { minWidth: '10vw' }; - }, label: collectionTrans('name'), }, { @@ -362,9 +359,6 @@ const Collections = () => { ); }, - getStyle: () => { - return { minWidth: '130px' }; - }, }, { id: 'rowCount', @@ -386,9 +380,6 @@ const Collections = () => { formatter(v) { return formatNumber(v.rowCount); }, - getStyle: () => { - return { minWidth: '100px' }; - }, }, { id: 'description', @@ -406,9 +397,6 @@ const Collections = () => { formatter(v) { return v.description || '--'; }, - getStyle: () => { - return { minWidth: '120px' }; - }, }, { id: 'createdTime', @@ -418,9 +406,6 @@ const Collections = () => { formatter(data) { return new Date(data.createdTime).toLocaleString(); }, - getStyle: () => { - return { minWidth: '165px' }; - }, }, ]; @@ -444,9 +429,6 @@ const Collections = () => { formatter(v) { return ; }, - getStyle: () => { - return { minWidth: '100px' }; - }, }); } diff --git a/client/src/pages/databases/collections/data/CollectionData.tsx b/client/src/pages/databases/collections/data/CollectionData.tsx index c95e0866..c3cb59c9 100644 --- a/client/src/pages/databases/collections/data/CollectionData.tsx +++ b/client/src/pages/databases/collections/data/CollectionData.tsx @@ -2,7 +2,6 @@ import { useState, useEffect, useRef, useContext, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { dataContext } from '@/context'; import { useQuery } from '@/hooks'; -import { getColumnWidth } from '@/utils'; import icons from '@/components/icons/Icons'; import AttuGrid from '@/components/grid/Grid'; import CustomToolBar from '@/components/grid/ToolBar'; @@ -38,6 +37,7 @@ const CollectionData = (props: CollectionDataProps) => { const [selectedData, setSelectedData] = useState([]); const exprInputRef = useRef(queryState.expr); const [, forceUpdate] = useState({}); + const loadingTimeoutRef = useRef(null); // UI functions const { fetchCollection } = useContext(dataContext); @@ -63,13 +63,27 @@ const CollectionData = (props: CollectionDataProps) => { } = useQuery({ collection, onQueryStart: (expr: string = '') => { - setTableLoading(true); + // Clear any existing timeout + if (loadingTimeoutRef.current) { + clearTimeout(loadingTimeoutRef.current); + } + + // Set a timeout to show loading after 100ms + loadingTimeoutRef.current = setTimeout(() => { + setTableLoading(true); + }, 100); + if (expr === '') { handleFilterReset(); return; } }, onQueryFinally: () => { + // Clear the timeout if it exists + if (loadingTimeoutRef.current) { + clearTimeout(loadingTimeoutRef.current); + loadingTimeoutRef.current = null; + } setTableLoading(false); }, queryState: queryState, @@ -212,8 +226,26 @@ const CollectionData = (props: CollectionDataProps) => { setSelectedData([]); exprInputRef.current = queryState.expr; forceUpdate({}); + + // Clean up timeout on unmount or when collection changes + return () => { + if (loadingTimeoutRef.current) { + clearTimeout(loadingTimeoutRef.current); + loadingTimeoutRef.current = null; + } + }; }, [collection.collection_name, queryState.expr]); + // Cleanup timeout on component unmount + useEffect(() => { + return () => { + if (loadingTimeoutRef.current) { + clearTimeout(loadingTimeoutRef.current); + loadingTimeoutRef.current = null; + } + }; + }, []); + return ( {collection && ( @@ -252,17 +284,6 @@ const CollectionData = (props: CollectionDataProps) => { ); }, - getStyle: d => { - const field = collection.schema.fields.find( - f => f.name === i - ); - if (!d || !field) { - return {}; - } - return { - minWidth: getColumnWidth(field), - }; - }, label: i === DYNAMIC_FIELD ? searchTrans('dynamicFields') : i, }; })} @@ -272,6 +293,8 @@ const CollectionData = (props: CollectionDataProps) => { rows={queryResult.data} rowCount={total} selected={selectedData} + tableHeaderHeight={43.5} + rowHeight={43} setSelected={onSelectChange} page={currentPage} onPageChange={handlePageChange} diff --git a/server/src/utils/Helper.ts b/server/src/utils/Helper.ts index 0793534b..2b95e256 100644 --- a/server/src/utils/Helper.ts +++ b/server/src/utils/Helper.ts @@ -41,7 +41,32 @@ export const makeRandomSparse = (dim: number) => { return sparseObject; }; +export const makeImageUrl = (): string => { + const sizes = [ + '200x150', + '300x200', + '400x300', + '500x400', + '600x450', + '800x600', + '1024x768', + '1200x800' + ]; + + const formats = ['jpg', 'png', 'gif']; + + const randomSize = sizes[Math.floor(Math.random() * sizes.length)]; + const randomFormat = formats[Math.floor(Math.random() * formats.length)]; + + return `https://dummyimage.com/${randomSize}.${randomFormat}`; +}; + export const makeRandomVarChar = (maxLength: number) => { + // 20% 的几率返回图片URL + if (Math.random() < 0.2) { + return makeImageUrl(); + } + const words = [ 'quick', 'brown',