feat: 更新 Img 和 Video 组件,优化视频处理逻辑,调整支付宝支付方案

This commit is contained in:
康猛 2025-12-26 15:18:07 +08:00
parent 93122d9955
commit 11f5907d4e
4 changed files with 93 additions and 65 deletions

View File

@ -52,7 +52,7 @@ const Img = forwardRef<ExpoImage, ImgProps>((props, ref) => {
const imgProps = {
style: [style, imageStyle],
ref,
placeholder: blurhash,
// placeholder: blurhash,
source: imageSource,
cachePolicy: 'memory-disk' as const,
errorSource,

View File

@ -1,5 +1,5 @@
import { Image } from 'expo-image'
import { memo, useEffect, useState } from 'react'
import { memo, useEffect, useMemo, useState } from 'react'
import { ViewStyle } from 'react-native'
import Video from 'react-native-video'
@ -7,19 +7,58 @@ type Props = {
url?: string
poster?: string
style?: ViewStyle
width?: number
} & React.ComponentProps<typeof Video>
const VideoBox = ({ url, poster, style, ...videoProps }: Props) => {
const [paused, setPaused] = useState(true)
const VideoBox = ({ url, poster, width = 120, style, ...videoProps }: Props) => {
const [urlFinal, setUrlFinal] = useState('')
const createUrl = (url: string) => {
return `https://modal-dev.bowong.cc/api/custom/video/converter/${encodeURI(url)}?options=compression_level=3,quality=70,loop=true,resolution=${width}x${width},fps=24`
}
const isImg = (url: any) => {
if (!url) return false
const lowerUrl = url.toLowerCase()
return lowerUrl?.match(/\.(jpg|jpeg|png|gif|webp|bmp|tiff|svg)(\?.*)?$/i)
}
async function resolveRedirect(url: string) {
const res = await fetch(url, {
method: 'GET',
headers: {
Range: 'bytes=0-0',
},
})
return res.url
}
const setRedirectUrl = async (url?: string) => {
const isImg2 = isImg(url)
if (isImg2) {
setUrlFinal(url!)
return
}
const webpUrl = createUrl(url!)
const finalUrl = await resolveRedirect(webpUrl)
// console.log('finalUrl-----------', finalUrl)
setUrlFinal(finalUrl!)
}
useEffect(() => {
console.log('url--------', url);
if (!url) return
setRedirectUrl(url!)
// const finalUrl = createUrl(url)
// console.log('finalUrl-----------', finalUrl)
setPaused(!Boolean(url))
// setUrlFinal(finalUrl)
return
}, [url])
const createUrl = (url: string)=>`https://modal-dev.bowong.cc/api/custom/video/converter/${encodeURI(url)}?options=compression_level=3,quality=70,loop=true,resolution=120x120,fps=24`
// console.log('urlFinal--------- ', urlFinal, url)
if (!url) return null
return (
// 当 url 变化时通过 key 强制重载,确保自动播放生效
// <Video
@ -36,7 +75,7 @@ const VideoBox = ({ url, poster, style, ...videoProps }: Props) => {
// style={style as any}
// {...videoProps}
// />
<Image source={{ uri: url ? createUrl(url) : '' }} style={style as any} />
<Image key={urlFinal} cachePolicy="memory-disk" source={{ uri: urlFinal }} style={style as any} />
)
}

View File

@ -4,7 +4,7 @@ import { Block, ConfirmModal, Text, Toast, VideoBox } from '@share/components'
import Img from '@share/components/Img'
import { AntDesign, FontAwesome, Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'
import * as ImagePicker from 'expo-image-picker'
import { Dimensions, ScrollView, View } from 'react-native'
import { Dimensions, Platform, ScrollView, View } from 'react-native'
import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { imgPicker } from '@/@share/apis'
@ -17,7 +17,7 @@ import { useFileUpload } from '@/hooks/actions/use-file-upload'
import { useTemplateActions } from '@/hooks/actions/use-template-actions'
import { useAuth } from '@/hooks/core/use-auth'
import { aniStorage } from '@/utils/aniStorage'
import { get } from 'react-native/Libraries/TurboModule/TurboModuleRegistry'
import * as FileSystem from 'expo-file-system'
// ============ 常量定义 ============
const BACKGROUND_VIDEOS = [
@ -115,11 +115,6 @@ const GridItem = memo(
style={{ transform: [{ skewX: '-6deg' }], height: itemWidth, width: itemWidth }}
>
<Block style={{ height: itemWidth, width: itemWidth }}>
{/* {isVideoUrl(post.imageUrl) ? (
<VideoBox url={post.imageUrl} style={{ height: itemWidth, width: itemWidth }} />
) : (
<Img src={post.imageUrl} className="h-full w-full" />
)} */}
<Img src={post.imageUrl} className="h-full w-full" />
</Block>
{isSelected && <Block className="absolute inset-[0px] border-[3px] border-accent" />}
@ -293,23 +288,17 @@ const TopCircleSection = memo(
)
const GalleryRenderer = memo(({ selectedItem }: { selectedItem: any }) => {
const uri = selectedItem?.url || selectedItem.imageUrl
console.log('GalleryRenderer------------', uri)
const url = selectedItem?.url || selectedItem.imageUrl
console.log('GalleryRenderer------------', url)
if (!uri) return null
if (!url) return null
const Width = 256
if (isVideoUrl(uri)) {
return (
<View style={{ width: Width, height: Width, borderRadius: Width, overflow: 'hidden' }} className="relative z-[10] border-[4px] border-black">
<VideoBox url={uri} style={{ width: Width, height: Width }} />
</View>
)
}
if (selectedItem?.type?.includes('video')) {
return <VideoBox url={uri} style={{ height: 256, width: 256 }} />
}
return <Img src={uri} className="h-full w-full" />
return (
<View style={{ width: Width, height: Width, borderRadius: Width, overflow: 'hidden' }} className="relative z-[10] border-[4px] border-black">
<VideoBox width={Width} url={url} style={{ width: Width, height: Width }} />
</View>
)
})
const ManagerView = memo(
@ -346,26 +335,11 @@ const ManagerView = memo(
)
const BackgroundBanner = memo(({ selectedItem }: { selectedItem: any }) => {
const uri = selectedItem?.url || selectedItem.imageUrl
// if (!uri) return null
console.log('BackgroundBanner----', uri)
// if (isVideoUrl(uri)) {
// return (
// <Block className="absolute inset-0 bottom-0 left-0 right-0 top-0 z-[0] bg-red-400">
// <VideoBox
// url={'https://cdn.roasmax.cn/upload/c6d501f816a24354ab652d2b9083a32b.mp4'}
// viewType={1}
// style={{ width: screenWidth, height: screenHeight }}
// />
// <Block className="absolute inset-0 bg-black/0" />
// </Block>
// )
// }
const url = selectedItem?.url || selectedItem.imageUrl
console.log('BackgroundBanner------------', url)
return (
<Block className="absolute inset-0 bottom-0 left-0 right-0 top-0 z-[10] overflow-hidden">
{/* <VideoBox url={BACKGROUND_VIDEOS[0]} style={{ width: screenWidth, height: screenHeight }} /> */}
<Img src={bgGif} style={{ width: screenWidth, height: screenHeight }} />
<VideoBox width={512} url={url} style={{ width: screenWidth, height: screenHeight }} />
</Block>
)
})
@ -399,7 +373,7 @@ const Sync = () => {
// 加载生成记录
useEffect(() => {
if (user?.id) {
loadGenerations({ userId: user.id })
loadGenerations()
}
}, [user?.id, loadGenerations])
@ -421,6 +395,20 @@ const Sync = () => {
}))
}, [generationsData, user])
useEffect(() => {
if (!selectedItem?.id && posts.length > 0) {
const firstItem = posts[0]
const newItem = {
id: firstItem.id,
imageUrl: firstItem.imageUrl,
url: firstItem.imageUrl,
originalUrl: firstItem.originalUrl,
}
setSelectedItem(newItem)
}
}, [posts])
const viewableIds = useRef<Set<string>>(new Set(posts.map((p: any) => p.id)))
// 动画效果
@ -505,21 +493,22 @@ const Sync = () => {
const handlePick = useCallback(async () => {
const assetList = await imgPicker({ maxImages: 1, type: ImagePicker.MediaTypeOptions.All, resultType: 'asset' })
if (!assetList?.length) return
const asset = assetList[0]
const isVideo = typeof asset === 'object' && asset?.type === 'video'
const uri = typeof asset === 'object' ? asset?.uri : asset
const result = assetList[0] as any
Toast.showLoading({ title: '上传中...', duration: 30e3 })
// 上传到云端
const fileBlob = await fetch(uri).then((r) => r.blob())
const mimeType = fileBlob.type || (isVideo ? 'video/mp4' : 'image/jpeg')
const file = {
name: result.fileName,
type: result.mimeType || 'image/jpeg',
uri: Platform.OS === 'android' ? result.uri : result.uri.replace('file://', ''),
}
const formData = new FormData()
formData.append('file', file as any)
const file = new File([fileBlob], fileName, { type: mimeType })
const { url, error } = await uploadFile(file)
const { url, error } = await uploadFile(file as any)
Toast.hideLoading()
@ -530,11 +519,11 @@ const Sync = () => {
const newItem = {
id: `local-${Date.now()}`,
type: isVideo ? 'video' : 'image',
// type: isVideo ? 'video' : 'image',
imageUrl: url,
url,
originalUrl: url,
...(typeof asset === 'object' ? asset : {}),
// ...(typeof asset === 'object' ? asset : {}),
}
setSelectedItem(newItem)
@ -629,7 +618,7 @@ const Sync = () => {
// 刷新列表
if (user?.id) {
loadGenerations({ userId: user.id })
loadGenerations()
}
}, [selectedItem, runTemplate, user?.id, loadGenerations])
@ -666,7 +655,7 @@ const Sync = () => {
// 刷新列表
if (user?.id) {
await loadGenerations({ userId: user.id })
await loadGenerations()
}
setSelectedIds(new Set())
@ -686,7 +675,7 @@ const Sync = () => {
viewableIds.current = new Set(viewableItems.map((v: any) => v.item.id))
}, [])
const renderHeaderFlatList = useMemo(
const renderHeader = useMemo(
() => (
<Block className="z-10">
<HeaderBanner connectedDevice={connectedDevice} onPick={handlePick} />
@ -732,7 +721,7 @@ const Sync = () => {
getItemType={() => 'row'}
removeClippedSubviews
keyExtractor={(item: any) => item?.id}
ListHeaderComponent={renderHeaderFlatList}
ListHeaderComponent={renderHeader}
renderItem={renderGridItem}
ItemSeparatorComponent={() => <Block style={{ height: 6 }} />}
contentContainerStyle={{ paddingHorizontal: 12, paddingBottom: 200 }}

View File

@ -10,7 +10,7 @@ import { Stack, useRouter } from 'expo-router'
import Alipay from 'expo-native-alipay'
import ExpoWeChat from 'expo-wechat'
import { alipay } from '@/lib/auth'
import { ANDROID_ID, IOS_UNIVERSAL_LINK } from '@/app.constants'
import { ANDROID_ID, IOS_UNIVERSAL_LINK, SCHEME } from '@/app.constants'
import { useUserBalance } from '@/hooks/core'
const { width: SCREEN_WIDTH } = Dimensions.get('window')
@ -44,7 +44,7 @@ export default function ChargePage() {
console.log('initpay ---------', Alipay)
Alipay.setAlipayScheme(ANDROID_ID)
Alipay.setAlipayScheme(SCHEME)
// ⚠️ 目前不可用,设置支付宝沙箱环境,仅 Android 支持
Alipay.setAlipaySandbox(true)