feat: 首页更新
This commit is contained in:
261
components/Image.tsx
Normal file
261
components/Image.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import React from 'react';
|
||||
import { Image as RNImage, ImageProps as RNImageProps, View, Text } from 'react-native';
|
||||
import { getImageSourceWithDefault, isLocalPath, convertLocalPathToUri } from '@/utils/image';
|
||||
import { getGameImageSizeFromPath } from '@/constants/gameImages';
|
||||
|
||||
/**
|
||||
* 自定义 Image 组件
|
||||
*
|
||||
* 功能:
|
||||
* - 自动处理图片 URL 验证
|
||||
* - 支持默认图片
|
||||
* - 支持自适应宽度或高度
|
||||
* - 支持自动测量图片尺寸(无需预知 aspectRatio)
|
||||
* - 简化图片加载逻辑
|
||||
*
|
||||
* 使用方式:
|
||||
* ```typescript
|
||||
* // 基础用法
|
||||
* <Image
|
||||
* source="https://example.com/image.png"
|
||||
* defaultSource="https://via.placeholder.com/200"
|
||||
* style={{ width: 100, height: 100 }}
|
||||
* />
|
||||
*
|
||||
* // 设置高度,宽度自适应(需要提供 aspectRatio)
|
||||
* <Image
|
||||
* source="https://example.com/image.png"
|
||||
* style={{ height: 200 }} // 只设置高度
|
||||
* aspectRatio={16 / 9}
|
||||
* adaptiveMode="height"
|
||||
* />
|
||||
*
|
||||
* // 设置宽度,高度自适应(需要提供 aspectRatio)
|
||||
* <Image
|
||||
* source="https://example.com/image.png"
|
||||
* style={{ width: 300 }} // 只设置宽度
|
||||
* aspectRatio={4 / 3}
|
||||
* adaptiveMode="width"
|
||||
* />
|
||||
*
|
||||
* // 自动测量图片尺寸(推荐用于未知尺寸的图片)
|
||||
* <Image
|
||||
* source="https://example.com/image.png"
|
||||
* style={{ height: 50 }}
|
||||
* adaptiveMode="height"
|
||||
* autoMeasure={true}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
|
||||
interface CustomImageProps extends Omit<RNImageProps, 'source' | 'defaultSource'> {
|
||||
/** 图片 URL(HTTP/HTTPS 或 Data URI) */
|
||||
source?: string;
|
||||
/** 默认图片 URL(当主图片无效时使用) */
|
||||
defaultSource?: string;
|
||||
/** 是否显示加载失败的占位符 */
|
||||
showPlaceholder?: boolean;
|
||||
/** 图片宽高比(用于自适应宽度或高度) */
|
||||
aspectRatio?: number;
|
||||
/** 自适应模式:
|
||||
* - 'height': 高度固定,宽度自适应(style 中只需设置 height)
|
||||
* - 'width': 宽度固定,高度自适应(style 中只需设置 width)
|
||||
*/
|
||||
adaptiveMode?: 'width' | 'height';
|
||||
/** 是否自动测量图片尺寸(当 aspectRatio 未提供时) */
|
||||
autoMeasure?: boolean;
|
||||
}
|
||||
|
||||
const Image = React.forwardRef<RNImage, CustomImageProps>(
|
||||
(
|
||||
{
|
||||
source,
|
||||
defaultSource = 'https://via.placeholder.com/200',
|
||||
showPlaceholder = false,
|
||||
style,
|
||||
aspectRatio,
|
||||
adaptiveMode,
|
||||
autoMeasure = false,
|
||||
onLoad,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
// 自动测量的宽高比状态
|
||||
const [measuredAspectRatio, setMeasuredAspectRatio] = React.useState<number | null>(null);
|
||||
|
||||
// 获取有效的图片 URI
|
||||
const imageUri = getImageSourceWithDefault(source, defaultSource);
|
||||
|
||||
// 处理图片加载完成事件
|
||||
const handleImageLoad = React.useCallback(
|
||||
(event: any) => {
|
||||
if (autoMeasure) {
|
||||
try {
|
||||
// 尝试从不同的位置获取图片尺寸
|
||||
let width: number | undefined;
|
||||
let height: number | undefined;
|
||||
|
||||
// 方式 1: event.nativeEvent.source
|
||||
if (event?.nativeEvent?.source) {
|
||||
width = event.nativeEvent.source.width;
|
||||
height = event.nativeEvent.source.height;
|
||||
}
|
||||
|
||||
// 方式 2: event.nativeEvent
|
||||
if (!width || !height) {
|
||||
width = event?.nativeEvent?.width;
|
||||
height = event?.nativeEvent?.height;
|
||||
}
|
||||
|
||||
// 方式 3: 直接从 event
|
||||
if (!width || !height) {
|
||||
width = event?.width;
|
||||
height = event?.height;
|
||||
}
|
||||
|
||||
if (width && height && typeof width === 'number' && typeof height === 'number') {
|
||||
const ratio = width / height;
|
||||
setMeasuredAspectRatio(ratio);
|
||||
if (__DEV__) {
|
||||
console.log(
|
||||
`[Image] Measured dimensions from event: ${width}x${height}, ratio: ${ratio.toFixed(3)}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 如果从 event 获取失败,尝试从本地路径的映射表获取
|
||||
if (isLocalPath(source)) {
|
||||
const size = getGameImageSizeFromPath(source);
|
||||
if (size && size.width && size.height) {
|
||||
const ratio = size.width / size.height;
|
||||
setMeasuredAspectRatio(ratio);
|
||||
if (__DEV__) {
|
||||
console.log(
|
||||
`[Image] Measured dimensions from mapping: ${size.width}x${size.height}, ratio: ${ratio.toFixed(3)}`
|
||||
);
|
||||
}
|
||||
} else if (__DEV__) {
|
||||
console.warn(
|
||||
`[Image] Failed to get dimensions from event or mapping for: ${source}`
|
||||
);
|
||||
}
|
||||
} else if (__DEV__) {
|
||||
console.warn(
|
||||
`[Image] Failed to get dimensions from event. Event:`,
|
||||
JSON.stringify(event, null, 2)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略获取尺寸的错误
|
||||
console.warn('Failed to measure image dimensions:', error);
|
||||
}
|
||||
}
|
||||
onLoad?.(event);
|
||||
},
|
||||
[autoMeasure, onLoad, source]
|
||||
);
|
||||
|
||||
// 计算自适应样式
|
||||
const adaptiveStyle = React.useMemo(() => {
|
||||
// 优先使用提供的 aspectRatio,其次使用测量的 aspectRatio
|
||||
const ratio = aspectRatio || (autoMeasure ? measuredAspectRatio : null);
|
||||
|
||||
if (!ratio || !adaptiveMode) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 从 style 中提取宽度和高度
|
||||
const styleObj = Array.isArray(style) ? Object.assign({}, ...style) : style || {};
|
||||
const fixedWidth = styleObj.width;
|
||||
const fixedHeight = styleObj.height;
|
||||
|
||||
// adaptiveMode 的含义:
|
||||
// - 'height': 高度固定,宽度自适应
|
||||
// 计算方式:width = height * aspectRatio
|
||||
// - 'width': 宽度固定,高度自适应
|
||||
// 计算方式:height = width / aspectRatio
|
||||
|
||||
if (adaptiveMode === 'height' && fixedHeight) {
|
||||
// 高度固定,计算宽度
|
||||
// 注意:不返回 aspectRatio,让宽高完全由计算值决定
|
||||
// 这样可以避免 resizeMode 对宽高比的影响
|
||||
const calculatedWidth = fixedHeight * ratio;
|
||||
if (__DEV__) {
|
||||
console.log(
|
||||
`[Image] adaptiveMode=height: fixedHeight=${fixedHeight}, ratio=${ratio.toFixed(3)}, calculatedWidth=${calculatedWidth.toFixed(1)}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
width: calculatedWidth,
|
||||
height: fixedHeight,
|
||||
};
|
||||
} else if (adaptiveMode === 'width' && fixedWidth) {
|
||||
// 宽度固定,计算高度
|
||||
// 注意:不返回 aspectRatio,让宽高完全由计算值决定
|
||||
const calculatedHeight = fixedWidth / ratio;
|
||||
if (__DEV__) {
|
||||
console.log(
|
||||
`[Image] adaptiveMode=width: fixedWidth=${fixedWidth}, ratio=${ratio.toFixed(3)}, calculatedHeight=${calculatedHeight.toFixed(1)}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
width: fixedWidth,
|
||||
height: calculatedHeight,
|
||||
};
|
||||
}
|
||||
|
||||
// 如果没有固定值,仅使用 aspectRatio
|
||||
return { aspectRatio: ratio };
|
||||
}, [aspectRatio, adaptiveMode, autoMeasure, measuredAspectRatio, style]);
|
||||
|
||||
// 如果没有有效的图片 URI,显示占位符
|
||||
if (!imageUri) {
|
||||
if (showPlaceholder) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
backgroundColor: '#f0f0f0',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
style,
|
||||
adaptiveStyle,
|
||||
]}
|
||||
>
|
||||
<Text style={{ color: '#999', fontSize: 12 }}>图片加载失败</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理本地路径和外部 URL
|
||||
let imageSource: any;
|
||||
|
||||
if (isLocalPath(imageUri)) {
|
||||
// 本地路径:/images/game/chess/color_101.png
|
||||
// 转换为 require 对象
|
||||
const localSource = convertLocalPathToUri(imageUri);
|
||||
imageSource = localSource || { uri: defaultSource };
|
||||
} else {
|
||||
// 外部 URL
|
||||
imageSource = { uri: imageUri };
|
||||
}
|
||||
|
||||
return (
|
||||
<RNImage
|
||||
ref={ref}
|
||||
source={imageSource}
|
||||
style={[style]}
|
||||
onLoad={handleImageLoad}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Image.displayName = 'Image';
|
||||
|
||||
export default Image;
|
||||
Reference in New Issue
Block a user