feat: 首页更新

This commit is contained in:
2025-11-13 16:47:10 +08:00
parent 9ef9233797
commit 54bf84b19b
1244 changed files with 3507 additions and 951 deletions

261
components/Image.tsx Normal file
View 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'> {
/** 图片 URLHTTP/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;