feat: 添加占位图支持,优化 Img 和 Video 组件,更新 OTA 升级逻辑
This commit is contained in:
parent
4268f1a905
commit
14c84de1ce
|
|
@ -1,5 +1,6 @@
|
||||||
import { Image as ExpoImage, type ImageProps as ExpoImageProps } from 'expo-image'
|
import { Image as ExpoImage, type ImageProps as ExpoImageProps } from 'expo-image'
|
||||||
import React, { forwardRef, memo, useMemo } from 'react'
|
import React, { forwardRef, memo, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { View } from 'react-native'
|
||||||
import tw from 'twrnc'
|
import tw from 'twrnc'
|
||||||
|
|
||||||
interface ImgProps extends ExpoImageProps {
|
interface ImgProps extends ExpoImageProps {
|
||||||
|
|
@ -11,6 +12,8 @@ interface ImgProps extends ExpoImageProps {
|
||||||
isCompression?: boolean
|
isCompression?: boolean
|
||||||
/** 自定义缓存键,用于需要重定向的 URL */
|
/** 自定义缓存键,用于需要重定向的 URL */
|
||||||
cacheKey?: string
|
cacheKey?: string
|
||||||
|
/** 占位图 URL,在真实图片加载完成前显示 */
|
||||||
|
placeholderSrc?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExpoImage.clearDiskCache()
|
// ExpoImage.clearDiskCache()
|
||||||
|
|
@ -24,15 +27,22 @@ const Img = forwardRef<ExpoImage, ImgProps>((props, ref) => {
|
||||||
className = '',
|
className = '',
|
||||||
style = {},
|
style = {},
|
||||||
src,
|
src,
|
||||||
errorSource,
|
|
||||||
source: propSource,
|
source: propSource,
|
||||||
cacheKey,
|
cacheKey,
|
||||||
width = 256,
|
width = 256,
|
||||||
isCompression = false,
|
isCompression = false,
|
||||||
isWebP = true,
|
isWebP = true,
|
||||||
|
placeholderSrc,
|
||||||
|
onLoad,
|
||||||
...reset
|
...reset
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoaded(false)
|
||||||
|
}, [src])
|
||||||
|
|
||||||
const imageStyle = tw`${className}`
|
const imageStyle = tw`${className}`
|
||||||
|
|
||||||
// 静态图全部压缩jpg
|
// 静态图全部压缩jpg
|
||||||
|
|
@ -49,47 +59,77 @@ const Img = forwardRef<ExpoImage, ImgProps>((props, ref) => {
|
||||||
|
|
||||||
// 构建图片源
|
// 构建图片源
|
||||||
const imageSource = useMemo(() => {
|
const imageSource = useMemo(() => {
|
||||||
// 如果提供了source属性,优先使用
|
|
||||||
if (propSource) return propSource
|
if (propSource) return propSource
|
||||||
|
|
||||||
if (!src) return undefined
|
if (!src) return undefined
|
||||||
|
|
||||||
if (typeof src === 'number') {
|
if (typeof src === 'number') {
|
||||||
// 本地图片资源(require导入的资源ID)
|
|
||||||
return src
|
return src
|
||||||
} else {
|
} else {
|
||||||
// 网络图片或本地文件路径
|
|
||||||
if (isNetworkImage(src)) {
|
if (isNetworkImage(src)) {
|
||||||
const finalUrl = isCompression ? compressionUrl : src
|
const finalUrl = isCompression ? compressionUrl : src
|
||||||
// console.log('finalUrl-------------', finalUrl)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uri: finalUrl,
|
uri: finalUrl,
|
||||||
cacheKey: cacheKey || finalUrl,
|
cacheKey: cacheKey || finalUrl,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 本地文件路径
|
|
||||||
return { uri: src }
|
return { uri: src }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [src, propSource, cacheKey, isCompression, compressionUrl])
|
}, [src, propSource, cacheKey, isCompression, compressionUrl])
|
||||||
|
|
||||||
const imgProps = {
|
const handleLoad = (e: any) => {
|
||||||
style: [style, imageStyle],
|
console.log('handleLoad--------------', e)
|
||||||
ref,
|
|
||||||
source: imageSource,
|
setIsLoaded(true)
|
||||||
// 使用 disk 缓存策略,减少内存占用
|
onLoad?.(e)
|
||||||
cachePolicy: 'disk' as const,
|
|
||||||
// 添加内存缓存上限,当内存紧张时优先释放
|
|
||||||
recyclingKey: typeof src === 'string' ? src : undefined,
|
|
||||||
errorSource,
|
|
||||||
transition: { duration: 200, effect: 'cross-dissolve' as const },
|
|
||||||
...reset,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ExpoImage {...imgProps} />
|
// 无占位图时直接返回原图
|
||||||
|
if (!placeholderSrc) {
|
||||||
|
return (
|
||||||
|
<ExpoImage
|
||||||
|
ref={ref}
|
||||||
|
style={[style, imageStyle]}
|
||||||
|
source={imageSource}
|
||||||
|
cachePolicy="disk"
|
||||||
|
recyclingKey={typeof src === 'string' ? src : undefined}
|
||||||
|
transition={{ duration: 200, effect: 'cross-dissolve' }}
|
||||||
|
onLoad={onLoad}
|
||||||
|
{...reset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// return <Image style={[style, imageStyle]} source={{ uri: compressionUrl(src as string) }} />
|
const showPlaceholder = placeholderSrc
|
||||||
|
|
||||||
|
// 有占位图时使用层叠布局
|
||||||
|
return (
|
||||||
|
<View style={[style, imageStyle, { overflow: 'hidden' }]}>
|
||||||
|
{/* 占位图层 - 加载完成后隐藏 */}
|
||||||
|
{showPlaceholder && (
|
||||||
|
<ExpoImage
|
||||||
|
style={{ position: 'absolute', width: '100%', height: '100%' }}
|
||||||
|
source={{ uri: placeholderSrc }}
|
||||||
|
cachePolicy="disk"
|
||||||
|
contentFit={reset.contentFit || 'cover'}
|
||||||
|
transition={{ duration: 100, effect: 'curl-up' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 真实图片层 */}
|
||||||
|
<ExpoImage
|
||||||
|
ref={ref}
|
||||||
|
style={{ width: '100%', height: '100%', opacity: isLoaded ? 1 : 0 }}
|
||||||
|
source={imageSource}
|
||||||
|
cachePolicy="disk"
|
||||||
|
recyclingKey={typeof src === 'string' ? src : undefined}
|
||||||
|
transition={{ duration: 300, effect: 'cross-dissolve' }}
|
||||||
|
onLoad={handleLoad}
|
||||||
|
contentFit={reset.contentFit || 'cover'}
|
||||||
|
{...reset}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
Img.displayName = 'Img'
|
Img.displayName = 'Img'
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import { Image, type ImageRef } from 'expo-image'
|
import { Image as ExpoImage, type ImageRef } from 'expo-image'
|
||||||
import { memo, useEffect, useRef, useState } from 'react'
|
import { memo, useEffect, useRef, useState } from 'react'
|
||||||
import { type ViewStyle } from 'react-native'
|
import { View, type ViewStyle } from 'react-native'
|
||||||
import Video from 'react-native-video'
|
import Video from 'react-native-video'
|
||||||
|
import tw from 'twrnc'
|
||||||
|
|
||||||
import { videoUrlCache } from '@/utils/storage'
|
import { videoUrlCache } from '@/utils/storage'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
className?: string
|
||||||
url?: string
|
url?: string
|
||||||
|
placeholderUrl?: string
|
||||||
needWeb?: boolean
|
needWeb?: boolean
|
||||||
style?: ViewStyle
|
style?: ViewStyle
|
||||||
width?: number
|
width?: number
|
||||||
|
|
@ -14,7 +17,16 @@ type Props = {
|
||||||
} & React.ComponentProps<typeof Video>
|
} & React.ComponentProps<typeof Video>
|
||||||
|
|
||||||
// 默认宽度256半屏宽度
|
// 默认宽度256半屏宽度
|
||||||
const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ...videoProps }: Props) => {
|
const VideoBox = ({
|
||||||
|
className = '',
|
||||||
|
url,
|
||||||
|
placeholderUrl = '',
|
||||||
|
needWeb = true,
|
||||||
|
width = 256,
|
||||||
|
style,
|
||||||
|
autoplay = true,
|
||||||
|
...videoProps
|
||||||
|
}: Props) => {
|
||||||
const [urlFinal, setUrlFinal] = useState('')
|
const [urlFinal, setUrlFinal] = useState('')
|
||||||
const imageRef = useRef<ImageRef | null>(null)
|
const imageRef = useRef<ImageRef | null>(null)
|
||||||
|
|
||||||
|
|
@ -114,7 +126,7 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ..
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Image
|
<ExpoImage
|
||||||
cachePolicy="disk"
|
cachePolicy="disk"
|
||||||
source={{ uri: url }}
|
source={{ uri: url }}
|
||||||
style={style as any}
|
style={style as any}
|
||||||
|
|
@ -123,9 +135,25 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ..
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageStyle = tw`${className}`
|
||||||
|
|
||||||
|
const showPlaceholder = !!placeholderUrl
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// 移除 key 避免组件重建导致闪烁,使用 transition 实现平滑切换
|
<View style={[style, imageStyle, { overflow: 'hidden' }]}>
|
||||||
<Image
|
{/* 占位图层 - 加载完成后隐藏 */}
|
||||||
|
{showPlaceholder && (
|
||||||
|
<ExpoImage
|
||||||
|
style={{ position: 'absolute', width: '100%', height: '100%' }}
|
||||||
|
source={{ uri: placeholderUrl }}
|
||||||
|
cachePolicy="disk"
|
||||||
|
contentFit={'cover'}
|
||||||
|
transition={{ duration: 100, effect: 'curl-up' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 真实图片层 */}
|
||||||
|
<ExpoImage
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
// 只使用 disk 缓存,减少内存占用
|
// 只使用 disk 缓存,减少内存占用
|
||||||
cachePolicy="disk"
|
cachePolicy="disk"
|
||||||
|
|
@ -136,6 +164,7 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ..
|
||||||
autoplay={autoplay}
|
autoplay={autoplay}
|
||||||
transition={{ duration: 200, effect: 'cross-dissolve' }}
|
transition={{ duration: 200, effect: 'cross-dissolve' }}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export const IOS_UNIVERSAL_LINK = 'duooomi.bowong.cn'
|
||||||
// 原生版本,原生代码变更时需要更新此版本号
|
// 原生版本,原生代码变更时需要更新此版本号
|
||||||
export const VERSION = '1.2.0'
|
export const VERSION = '1.2.0'
|
||||||
// JavaScript版本,JS代码变更时需要更新此版本号
|
// JavaScript版本,JS代码变更时需要更新此版本号
|
||||||
export const APP_VERSION = 'dev202601211414'
|
export const APP_VERSION = 'dev202601221539'
|
||||||
|
|
||||||
const ALIPAY_SCHEMA = 'alipay2021006119657394'
|
const ALIPAY_SCHEMA = 'alipay2021006119657394'
|
||||||
const ALIPAY_SCHEMA_SANDBOX = 'alipay9021000158673972'
|
const ALIPAY_SCHEMA_SANDBOX = 'alipay9021000158673972'
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export default observer(function TabTwoScreen() {
|
||||||
const [otaUrl, setOtaUrl] = useState(
|
const [otaUrl, setOtaUrl] = useState(
|
||||||
userStore.scannedQR ?? 'https://cdn.roasmax.cn/upload/bf25206ab8644a8fb914aad5cf0fca08.bin',
|
userStore.scannedQR ?? 'https://cdn.roasmax.cn/upload/bf25206ab8644a8fb914aad5cf0fca08.bin',
|
||||||
)
|
)
|
||||||
|
const [comType, setComType] = useState('0x02') // 默认OTA_PACKAGE类型为128
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userStore.scannedQR) {
|
if (userStore.scannedQR) {
|
||||||
|
|
@ -138,10 +139,10 @@ export default observer(function TabTwoScreen() {
|
||||||
renderContent: () => <SyncProgressToast title="OTA升级中" />,
|
renderContent: () => <SyncProgressToast title="OTA升级中" />,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
})
|
})
|
||||||
const buffer = await bleManager.performOtaUpgrade(otaUrl, (progress) => {
|
const buffer = await bleManager.performOtaUpgrade(otaUrl, comType)
|
||||||
// 进度在 bleStore.transferProgress 中同步显示,这里仅可选处理
|
|
||||||
})
|
Toast.hide()
|
||||||
Toast?.show({ title: `OTA升级完成 (${buffer.byteLength} bytes) ` })
|
Toast?.show({ title: `OTA升级完成 (${buffer.byteLength} bytes) `, duration: 2e3 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('OTA upgrade failed:', error)
|
console.error('OTA upgrade failed:', error)
|
||||||
const msg = typeof error === 'string' ? error : (error as any)?.message || 'OTA升级失败'
|
const msg = typeof error === 'string' ? error : (error as any)?.message || 'OTA升级失败'
|
||||||
|
|
@ -426,6 +427,17 @@ export default observer(function TabTwoScreen() {
|
||||||
/>
|
/>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
|
|
||||||
|
<ThemedView style={{ marginTop: 8 }}>
|
||||||
|
<ThemedText style={{ marginBottom: 4 }}>蓝牙头设置 Command Type:</ThemedText>
|
||||||
|
<TextInput
|
||||||
|
placeholder="蓝牙头"
|
||||||
|
style={styles.input}
|
||||||
|
value={comType}
|
||||||
|
onChangeText={setComType}
|
||||||
|
placeholderTextColor="#999"
|
||||||
|
/>
|
||||||
|
</ThemedView>
|
||||||
|
|
||||||
<ThemedView style={{ marginTop: 8 }}>
|
<ThemedView style={{ marginTop: 8 }}>
|
||||||
<ThemedText style={{ marginBottom: 4 }}>OTA URL:</ThemedText>
|
<ThemedText style={{ marginBottom: 4 }}>OTA URL:</ThemedText>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|
|
||||||
|
|
@ -598,6 +598,7 @@ const HeroCircle = observer<HeroCircleProps>(function HeroCircle({ selectedItem,
|
||||||
|
|
||||||
const Width = 216
|
const Width = 216
|
||||||
const previewUrl = selectedItem?.webpHighPreviewUrl || selectedItem?.webpPreviewUrl || ''
|
const previewUrl = selectedItem?.webpHighPreviewUrl || selectedItem?.webpPreviewUrl || ''
|
||||||
|
const placeholderUrl = selectedItem?.webpPreviewUrl || ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Block className="relative z-10 items-center">
|
<Block className="relative z-10 items-center">
|
||||||
|
|
@ -605,7 +606,12 @@ const HeroCircle = observer<HeroCircleProps>(function HeroCircle({ selectedItem,
|
||||||
<Block className="flex-1">
|
<Block className="flex-1">
|
||||||
<Block className="relative z-10 mt-[20px] flex size-[224px] rounded-full border-4 border-black shadow-deep-black">
|
<Block className="relative z-10 mt-[20px] flex size-[224px] rounded-full border-4 border-black shadow-deep-black">
|
||||||
<Block className="relative size-full overflow-hidden rounded-full">
|
<Block className="relative size-full overflow-hidden rounded-full">
|
||||||
<Img style={{ height: Width, width: Width, borderRadius: Width }} width={256} src={previewUrl} />
|
<Img
|
||||||
|
style={{ height: Width, width: Width, borderRadius: Width }}
|
||||||
|
width={256}
|
||||||
|
placeholderSrc={placeholderUrl}
|
||||||
|
src={previewUrl}
|
||||||
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
<Block className="pointer-events-none absolute inset-0 rounded-full border-2 border-black/10" />
|
<Block className="pointer-events-none absolute inset-0 rounded-full border-2 border-black/10" />
|
||||||
|
|
|
||||||
|
|
@ -956,7 +956,7 @@ const TopCircleSection = memo(
|
||||||
|
|
||||||
const GalleryRenderer = memo(({ selectedItem }: { selectedItem: any }) => {
|
const GalleryRenderer = memo(({ selectedItem }: { selectedItem: any }) => {
|
||||||
const url = selectedItem?.url || selectedItem?.imageUrl
|
const url = selectedItem?.url || selectedItem?.imageUrl
|
||||||
|
const placeholderUrl = selectedItem?.imageUrl
|
||||||
// console.log('GalleryRenderer--------------', selectedItem)
|
// console.log('GalleryRenderer--------------', selectedItem)
|
||||||
|
|
||||||
if (!url) return null
|
if (!url) return null
|
||||||
|
|
@ -967,7 +967,7 @@ const GalleryRenderer = memo(({ selectedItem }: { selectedItem: any }) => {
|
||||||
className="relative z-10 border-4 border-black"
|
className="relative z-10 border-4 border-black"
|
||||||
style={{ width: Width, height: Width, borderRadius: Width, overflow: 'hidden' }}
|
style={{ width: Width, height: Width, borderRadius: Width, overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<VideoBox style={{ width: Width, height: Width }} width={Width} url={url} />
|
<VideoBox style={{ width: Width, height: Width }} width={Width} placeholderUrl={placeholderUrl} url={url} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,8 @@ const ChargePage = observer(function ChargePage() {
|
||||||
const { data, error } = await handleError(
|
const { data, error } = await handleError(
|
||||||
async () =>
|
async () =>
|
||||||
await alipay.preRecharge({
|
await alipay.preRecharge({
|
||||||
credits: 2,
|
// credits: 2,
|
||||||
// credits: parseInt(selectedRecharge?.points.replace(/,/g, '')),
|
credits: parseInt(selectedRecharge?.points.replace(/,/g, '')),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -649,7 +649,11 @@ class BleManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async performOtaUpgrade(url: string, onProgress?: (progress: number) => void): Promise<ArrayBuffer> {
|
async performOtaUpgrade(
|
||||||
|
url: string,
|
||||||
|
comType = '0x02',
|
||||||
|
onProgress?: (progress: number) => void,
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
const state = bleStore.state
|
const state = bleStore.state
|
||||||
if (!state.connectedDevice) {
|
if (!state.connectedDevice) {
|
||||||
const error = 'No device connected'
|
const error = 'No device connected'
|
||||||
|
|
@ -687,7 +691,8 @@ class BleManager {
|
||||||
await this.fileTransferService.transferFile(
|
await this.fileTransferService.transferFile(
|
||||||
state.connectedDevice.id,
|
state.connectedDevice.id,
|
||||||
arrayBuffer,
|
arrayBuffer,
|
||||||
COMMAND_TYPES.OTA_PACKAGE,
|
// COMMAND_TYPES.OTA_PACKAGE,
|
||||||
|
Number(comType),
|
||||||
(progress) => {
|
(progress) => {
|
||||||
bleStore.setState((prev) => ({ ...prev, transferProgress: progress * 100 }))
|
bleStore.setState((prev) => ({ ...prev, transferProgress: progress * 100 }))
|
||||||
onProgress?.(progress)
|
onProgress?.(progress)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export const COMMAND_TYPES = {
|
||||||
PREPARE_TRANSFER: 0x14,
|
PREPARE_TRANSFER: 0x14,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type APP_COMMAND_TYPES = (typeof COMMAND_TYPES)[keyof typeof COMMAND_TYPES]
|
export type APP_COMMAND_TYPES = (typeof COMMAND_TYPES)[keyof typeof COMMAND_TYPES] | number
|
||||||
|
|
||||||
export const EVENT_TYPES = {
|
export const EVENT_TYPES = {
|
||||||
TRANSFER_OTA_PACKAGE: {
|
TRANSFER_OTA_PACKAGE: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue