@@ -77,6 +77,15 @@ function formatGpsSpeed(speed: number, ref?: string): string {
7777 return `${ speed . toFixed ( 1 ) } ${ unit } ` ;
7878}
7979
80+ function getExifOrientation ( exif : unknown ) : number | undefined {
81+ if ( ! exif || typeof exif !== "object" || Array . isArray ( exif ) ) {
82+ return undefined ;
83+ }
84+
85+ const orientation = ( exif as { orientation ?: unknown } ) . orientation ;
86+ return typeof orientation === "number" ? orientation : undefined ;
87+ }
88+
8089interface PhotoWithMedia {
8190 id : number ;
8291 slug : string ;
@@ -126,6 +135,10 @@ export default function PhotoModalClient({
126135 const [ isOpen , setIsOpen ] = useState ( true ) ;
127136 const [ isClosing , setIsClosing ] = useState ( false ) ;
128137 const [ hdImageLoaded , setHdImageLoaded ] = useState ( false ) ;
138+ const [ loadedImageDimensions , setLoadedImageDimensions ] = useState < {
139+ width : number ;
140+ height : number ;
141+ } | null > ( null ) ;
129142 const [ openAnimationComplete , setOpenAnimationComplete ] = useState ( false ) ;
130143 const [ mounted , setMounted ] = useState ( false ) ;
131144
@@ -144,6 +157,41 @@ export default function PhotoModalClient({
144157 const thumbnailUrl = useGalleryLightboxStore ( ( s ) => s . thumbnailUrl ) ;
145158 const clearStore = useGalleryLightboxStore ( ( s ) => s . clear ) ;
146159
160+ const metadataImageDimensions = useMemo ( ( ) => {
161+ const width = photo . media . width || 800 ;
162+ const height = photo . media . height || 600 ;
163+ const orientation = getExifOrientation ( photo . media . exif ) ;
164+
165+ // EXIF 5-8 表示图片需要旋转 90 度,数据库中的宽高仍可能是旋转前的值。
166+ if ( orientation !== undefined && orientation >= 5 && orientation <= 8 ) {
167+ return { width : height , height : width } ;
168+ }
169+
170+ return { width, height } ;
171+ } , [ photo . media . exif , photo . media . height , photo . media . width ] ) ;
172+
173+ const handleHdImageLoad = useCallback (
174+ ( event : React . SyntheticEvent < HTMLImageElement > ) => {
175+ const { naturalWidth, naturalHeight } = event . currentTarget ;
176+
177+ if ( naturalWidth > 0 && naturalHeight > 0 ) {
178+ setLoadedImageDimensions ( ( previous ) => {
179+ if (
180+ previous ?. width === naturalWidth &&
181+ previous . height === naturalHeight
182+ ) {
183+ return previous ;
184+ }
185+
186+ return { width : naturalWidth , height : naturalHeight } ;
187+ } ) ;
188+ }
189+
190+ setHdImageLoaded ( true ) ;
191+ } ,
192+ [ ] ,
193+ ) ;
194+
147195 // 隐藏原图
148196 const hideOriginalImage = useCallback ( ( ) => {
149197 if ( openedPhotoId !== null ) {
@@ -386,8 +434,8 @@ export default function PhotoModalClient({
386434 }
387435
388436 // 图片原始尺寸
389- const naturalWidth = photo . media . width || 800 ;
390- const naturalHeight = photo . media . height || 600 ;
437+ const { width : naturalWidth , height : naturalHeight } =
438+ loadedImageDimensions || metadataImageDimensions ;
391439
392440 // 计算适配的尺寸
393441 const widthRatio = maxImageWidth / naturalWidth ;
@@ -446,11 +494,15 @@ export default function PhotoModalClient({
446494 containerX,
447495 containerY,
448496 } ;
449- } , [ photo . media . width , photo . media . height , sourceRect , isMobile ] ) ;
497+ } , [ loadedImageDimensions , metadataImageDimensions , sourceRect , isMobile ] ) ;
450498
451499 // 如果没有 geometry,不渲染
452500 if ( ! geometry ) return null ;
453501
502+ // 过渡期间需要填满源卡片,避免宽高比变化时出现黑边;静止后再完整显示原图。
503+ const modalImageObjectFit =
504+ openAnimationComplete && ! isClosing ? "object-contain" : "object-cover" ;
505+
454506 // 上传者信息
455507 const uploader = photo . media . user ;
456508
@@ -970,7 +1022,7 @@ export default function PhotoModalClient({
9701022 initial = { { opacity : 1 } }
9711023 exit = { { opacity : 0 } }
9721024 transition = { { duration : 0.3 } }
973- className = " absolute inset-0 w-full h-full object-cover z-[1]"
1025+ className = { ` absolute inset-0 w-full h-full ${ modalImageObjectFit } z-[1]` }
9741026 draggable = { false }
9751027 onLoad = { handleThumbnailLoad }
9761028 />
@@ -983,10 +1035,10 @@ export default function PhotoModalClient({
9831035 fill
9841036 sizes = "(max-width: 768px) 100vw, 80vw"
9851037 priority
986- className = { `absolute inset-0 w-full h-full object-cover transition-opacity duration-300 z-[2] ${
1038+ className = { `absolute inset-0 w-full h-full ${ modalImageObjectFit } transition-opacity duration-300 z-[2] ${
9871039 hdImageLoaded ? "opacity-100" : "opacity-0"
9881040 } `}
989- onLoad = { ( ) => setHdImageLoaded ( true ) }
1041+ onLoad = { handleHdImageLoad }
9901042 data-lightbox
9911043 />
9921044 </ motion . div >
0 commit comments