705 lines
22 KiB
TypeScript
705 lines
22 KiB
TypeScript
import { Ionicons } from '@expo/vector-icons'
|
||
import { Block, ConfirmModal, ListEmpty, Text, Toast, VideoBox } from '@share/components'
|
||
import { FlashList } from '@shopify/flash-list'
|
||
import { LinearGradient } from 'expo-linear-gradient'
|
||
import { router, useFocusEffect } from 'expo-router'
|
||
import { observer } from 'mobx-react-lite'
|
||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||
import { ActivityIndicator, RefreshControl, TextInput } from 'react-native'
|
||
|
||
import BannerSection from '@/components/BannerSection'
|
||
import { useTemplateActions } from '@/hooks/actions/use-template-actions'
|
||
import { useTemplates } from '@/hooks/data'
|
||
import { useFavoriteTemplates } from '@/hooks/data/use-favorite-templates'
|
||
import { userBalanceStore, userStore } from '@/stores'
|
||
import { screenWidth } from '@/utils'
|
||
import { cn } from '@/utils/cn'
|
||
|
||
const CATEGORY_ID = process.env.EXPO_PUBLIC_INDEX_GROUP_ID
|
||
const ITEM_WIDTH = Math.floor((screenWidth - 24 - 12 * 2) / 3)
|
||
const PAGE_SIZE = 21
|
||
|
||
type MediaItem = {
|
||
id: string
|
||
type: 'image' | 'video'
|
||
url: string
|
||
poster?: string
|
||
likeCount?: number
|
||
price: number
|
||
title?: string
|
||
authorName?: string
|
||
}
|
||
type ActiveTab = 'gen' | '' | 'new' | 'like'
|
||
|
||
/** =========================
|
||
* Entry page
|
||
* ========================= */
|
||
|
||
const Index = observer(function Index() {
|
||
// 从MobX Store获取用户信息
|
||
const { user, isAuthenticated } = userStore as typeof userStore
|
||
const { execute: loadTemplates } = useTemplates()
|
||
const { execute: loadFavorites } = useFavoriteTemplates()
|
||
const { runTemplate } = useTemplateActions()
|
||
|
||
/** ================= 状态 ================= */
|
||
|
||
const [activeTab, setActiveTab] = useState<ActiveTab>('')
|
||
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
||
|
||
const [allItems, setAllItems] = useState<MediaItem[]>([])
|
||
const [selectedItem, setSelectedItem] = useState<MediaItem | null>(null)
|
||
|
||
const [refreshing, setRefreshing] = useState(false)
|
||
const [loadingMore, setLoadingMore] = useState(false)
|
||
const [hasMore, setHasMore] = useState(true)
|
||
|
||
/** ================= refs(核心) ================= */
|
||
|
||
const queryRef = useRef({
|
||
page: 1,
|
||
pageSize: PAGE_SIZE,
|
||
search: '',
|
||
tab: '' as ActiveTab,
|
||
})
|
||
|
||
const loadingRef = useRef(false)
|
||
const firstLoadDoneRef = useRef(false)
|
||
|
||
/** ================= 工具函数 ================= */
|
||
|
||
type TemplateData = {
|
||
id: string
|
||
previewUrl?: string
|
||
coverImageUrl?: string
|
||
likeCount?: number
|
||
price?: number
|
||
title?: string
|
||
isDeleted?: boolean
|
||
}
|
||
|
||
const transformTemplateToMediaItem = useCallback((template: TemplateData): MediaItem => {
|
||
const isVideo = template.previewUrl?.includes('.mp4')
|
||
return {
|
||
id: template.id,
|
||
type: isVideo ? 'video' : 'image',
|
||
url: isVideo
|
||
? (template?.webpPreviewUrl ?? template?.previewUrl)
|
||
: template.coverImageUrl || template.previewUrl || '',
|
||
poster: isVideo ? template.coverImageUrl : undefined,
|
||
likeCount: template.likeCount || 0,
|
||
price: template.price || 0,
|
||
authorName: template?.user?.name || '未知作者',
|
||
title: template.title || '',
|
||
}
|
||
}, [])
|
||
|
||
/** ================= 统一请求函数 ================= */
|
||
|
||
const fetchList = async (mode: 'init' | 'refresh' | 'loadMore') => {
|
||
if (loadingRef.current) return
|
||
if (mode === 'loadMore' && !hasMore) return
|
||
|
||
loadingRef.current = true
|
||
if (mode === 'loadMore') {
|
||
setLoadingMore(true)
|
||
} else {
|
||
setRefreshing(true)
|
||
}
|
||
|
||
try {
|
||
const { page, pageSize, search, tab } = queryRef.current
|
||
let newItems: MediaItem[] = []
|
||
|
||
if (tab === 'like') {
|
||
console.log('加载收藏列表,isAuthenticated=', isAuthenticated)
|
||
if (!isAuthenticated) {
|
||
setHasMore(false)
|
||
newItems = []
|
||
} else {
|
||
const { data } = await loadFavorites({ page, limit: pageSize })
|
||
newItems =
|
||
data?.favorites
|
||
?.filter((f) => f.template && !(f.template as TemplateData).isDeleted)
|
||
.map((f) => transformTemplateToMediaItem(f.template as TemplateData)) || []
|
||
}
|
||
} else {
|
||
const sortBy = tab === 'new' ? 'createdAt' : 'likeCount'
|
||
const { data } = await loadTemplates({
|
||
page,
|
||
limit: pageSize,
|
||
sortBy,
|
||
sortOrder: 'desc',
|
||
search,
|
||
categoryId: CATEGORY_ID,
|
||
})
|
||
newItems = data?.templates?.map(transformTemplateToMediaItem) || []
|
||
}
|
||
|
||
if (newItems.length < pageSize) {
|
||
setHasMore(false)
|
||
}
|
||
|
||
const updatedItems = mode === 'loadMore' ? [...allItems, ...newItems] : newItems
|
||
|
||
setAllItems(updatedItems)
|
||
} catch (e) {
|
||
console.error('fetchList error:', e)
|
||
setHasMore(false)
|
||
} finally {
|
||
loadingRef.current = false
|
||
// 标记首次(或第一页)加载已完成,避免 FlashList 首次渲染触发 onEndReached
|
||
const currentPage = queryRef.current.page
|
||
if (currentPage === 1) {
|
||
firstLoadDoneRef.current = true
|
||
}
|
||
setRefreshing(false)
|
||
setLoadingMore(false)
|
||
}
|
||
}
|
||
|
||
/** ================= tab 切换(抽象后的重点) ================= */
|
||
|
||
const changeTab = useCallback(
|
||
(tab: ActiveTab) => {
|
||
// 重置加载状态,确保新的tab切换不会被旧的加载状态阻挡
|
||
loadingRef.current = false
|
||
queryRef.current = {
|
||
...queryRef.current,
|
||
tab,
|
||
page: 1,
|
||
}
|
||
setHasMore(true)
|
||
firstLoadDoneRef.current = false
|
||
fetchList('init')
|
||
},
|
||
[isAuthenticated],
|
||
)
|
||
|
||
/** ================= 搜索 ================= */
|
||
|
||
const handleSearch = useCallback((text: string) => {
|
||
queryRef.current = {
|
||
...queryRef.current,
|
||
search: text,
|
||
page: 1,
|
||
}
|
||
setHasMore(true)
|
||
firstLoadDoneRef.current = false
|
||
fetchList('refresh')
|
||
}, [])
|
||
|
||
/** ================= 刷新 / 加载更多 ================= */
|
||
|
||
const handleRefresh = useCallback(() => {
|
||
queryRef.current.page = 1
|
||
setHasMore(true)
|
||
firstLoadDoneRef.current = false
|
||
fetchList('refresh')
|
||
}, [])
|
||
|
||
const handleLoadMore = useCallback(() => {
|
||
// 防止重复请求或首次渲染时误触发
|
||
if (loadingRef.current || !hasMore || refreshing || loadingMore) return
|
||
if (!firstLoadDoneRef.current) return
|
||
// if (allItems.length === 0) return // 首次加载未完成时不触发加载更多
|
||
|
||
queryRef.current.page += 1
|
||
fetchList('loadMore')
|
||
console.log('handleLoadMore-------------')
|
||
}, [hasMore, refreshing, loadingMore])
|
||
|
||
/** ================= selectedItem 修正 ================= */
|
||
|
||
useEffect(() => {
|
||
if (!allItems.length) {
|
||
setSelectedItem(null)
|
||
return
|
||
}
|
||
if (!allItems.find((i) => i.id === selectedItem?.id)) {
|
||
setSelectedItem(allItems[0])
|
||
}
|
||
}, [allItems, selectedItem?.id])
|
||
|
||
useEffect(() => {
|
||
fetchList('init')
|
||
// 初次加载时直接加载余额
|
||
userBalanceStore.load(true)
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [])
|
||
|
||
// 管理轮询:用户登录时启动,登出时停止,使用延迟判断避免刷新时的临时状态
|
||
useEffect(() => {
|
||
// 使用短暂延迟,避免刷新时的临时状态变化
|
||
const timer = setTimeout(() => {
|
||
if (isAuthenticated && user?.id) {
|
||
console.log('用户已登录,启动余额轮询')
|
||
userBalanceStore.startPolling()
|
||
} else {
|
||
console.log('用户未登录,停止余额轮询')
|
||
userBalanceStore.stopPolling()
|
||
}
|
||
}, 100) // 100ms延迟,避免刷新时的快速状态切换
|
||
|
||
return () => {
|
||
clearTimeout(timer)
|
||
}
|
||
}, [isAuthenticated, user?.id])
|
||
|
||
// 组件卸载时确保轮询停止
|
||
useEffect(() => {
|
||
return () => {
|
||
userBalanceStore.stopPolling()
|
||
}
|
||
}, [])
|
||
|
||
useFocusEffect(() => {
|
||
if (isAuthenticated && user?.id) {
|
||
userBalanceStore.load(true)
|
||
}
|
||
})
|
||
|
||
/** ================= 快速生成 ================= */
|
||
|
||
const handleQuickGen = async () => {
|
||
if (!isAuthenticated) {
|
||
router.push('/auth')
|
||
return
|
||
}
|
||
if (!selectedItem) {
|
||
Toast.show({ title: '请先选择一个模板' })
|
||
return
|
||
}
|
||
|
||
// 显示加载状态并刷新余额
|
||
Toast.showLoading()
|
||
try {
|
||
await userBalanceStore.load(true) // 生成前刷新余额
|
||
|
||
// 使用最新的余额数据进行检查
|
||
const currentBalance = userBalanceStore.balance
|
||
if (currentBalance < selectedItem.price) {
|
||
Toast.show({ title: '余额不足,请充值' })
|
||
return
|
||
}
|
||
} catch (error) {
|
||
Toast.show({ title: '余额加载失败,请重试' })
|
||
return
|
||
} finally {
|
||
Toast.hideLoading()
|
||
}
|
||
|
||
Toast.showModal(
|
||
<ConfirmModal
|
||
content={
|
||
<Text className="text-[14px] font-bold">
|
||
生成将消耗
|
||
<Text className="mx-[4px] text-[20px] text-[#e61e25]">{selectedItem.price}</Text>
|
||
</Text>
|
||
}
|
||
onCancel={Toast.hideModal}
|
||
onConfirm={async () => {
|
||
Toast.hideModal()
|
||
Toast.showLoading()
|
||
|
||
try {
|
||
// 先进行乐观更新,扣减余额
|
||
userBalanceStore.deductBalance(selectedItem.price)
|
||
|
||
const { generationId, error } = await runTemplate({
|
||
templateId: selectedItem.id,
|
||
data: {},
|
||
})
|
||
|
||
if (generationId && user?.id) {
|
||
// 生成成功后强制刷新余额以获取准确数据
|
||
await userBalanceStore.load(true)
|
||
Toast.show({ title: '生成任务开启,请在我的生成中查看' })
|
||
} else {
|
||
// 生成失败,恢复余额
|
||
userBalanceStore.setBalance(userBalanceStore.balance + selectedItem.price)
|
||
Toast.show({ title: error?.message || '生成失败' })
|
||
}
|
||
} catch (error) {
|
||
// 异常情况下恢复余额
|
||
userBalanceStore.setBalance(userBalanceStore.balance + selectedItem.price)
|
||
Toast.show({ title: '网络异常,请重试' })
|
||
} finally {
|
||
Toast.hideLoading()
|
||
}
|
||
}}
|
||
/>,
|
||
)
|
||
}
|
||
|
||
const renderListEmpty = () => {
|
||
if (activeTab === 'like' && !isAuthenticated) {
|
||
return (
|
||
<Block className="mt-[40px] items-center justify-center gap-[16px] py-[60px]">
|
||
<Block className="size-[80px] items-center justify-center rounded-full border-4 border-white/20 bg-white/10">
|
||
<Ionicons color="rgba(255,255,255,0.6)" name="heart-outline" size={40} />
|
||
</Block>
|
||
<Text className="text-[16px] font-bold text-white/80">查看您收藏的模板</Text>
|
||
<Text className="text-[12px] text-white/50">登录后即可查看和管理收藏</Text>
|
||
<Block
|
||
className="mt-[12px] flex-row items-center gap-[8px] border-[3px] border-white bg-accent px-[32px] py-[12px] shadow-[4px_4px_0px_rgba(255,255,255,0.2)]"
|
||
style={{ transform: [{ skewX: '-6deg' }] }}
|
||
onClick={() => router.push('/auth')}
|
||
>
|
||
<Ionicons color="black" name="flash" size={16} style={{ transform: [{ skewX: '6deg' }] }} />
|
||
<Text className="font-900 text-[14px] text-black" style={{ transform: [{ skewX: '6deg' }] }}>
|
||
立即登录
|
||
</Text>
|
||
</Block>
|
||
</Block>
|
||
)
|
||
} else {
|
||
return <ListEmpty />
|
||
}
|
||
}
|
||
|
||
// 缓存渲染函数以提升性能
|
||
const renderItem = useCallback(
|
||
({ item }: { item: MediaItem }) => (
|
||
<GridItem
|
||
isSelected={selectedItem?.id === item.id}
|
||
item={item}
|
||
itemWidth={ITEM_WIDTH}
|
||
onSelect={() => setSelectedItem(item)}
|
||
/>
|
||
),
|
||
[selectedItem?.id],
|
||
)
|
||
|
||
const keyExtractor = useCallback((item: MediaItem) => item.id, [])
|
||
|
||
/** ================= UI ================= */
|
||
return (
|
||
<Block className="flex-1 bg-black px-[12px]">
|
||
<BannerSection />
|
||
|
||
<HeroCircle onOpenSearch={() => setIsSearchOpen(true)} onQuickGen={handleQuickGen} selectedItem={selectedItem} />
|
||
|
||
<FilterSection
|
||
activeTab={activeTab}
|
||
onChange={(tab) => {
|
||
setActiveTab(tab)
|
||
changeTab(tab)
|
||
}}
|
||
/>
|
||
|
||
<FlashList
|
||
contentContainerStyle={{ paddingBottom: 200, marginTop: 12 }}
|
||
ListFooterComponent={
|
||
loadingMore ? (
|
||
<Block className="items-center py-[20px]">
|
||
<ActivityIndicator color="#FFE500" />
|
||
</Block>
|
||
) : null
|
||
}
|
||
numColumns={3}
|
||
onEndReached={handleLoadMore}
|
||
drawDistance={1200}
|
||
onEndReachedThreshold={0.3}
|
||
refreshControl={<RefreshControl colors={['#FFE500']} refreshing={refreshing} onRefresh={handleRefresh} />}
|
||
renderItem={renderItem}
|
||
keyExtractor={keyExtractor}
|
||
ListEmptyComponent={renderListEmpty}
|
||
showsVerticalScrollIndicator={false}
|
||
data={allItems}
|
||
// @ts-ignore
|
||
estimatedItemSize={ITEM_WIDTH}
|
||
/>
|
||
|
||
<SearchOverlay isOpen={isSearchOpen} onClose={() => setIsSearchOpen(false)} onSearch={handleSearch} />
|
||
</Block>
|
||
)
|
||
})
|
||
|
||
type SearchOverlayProps = {
|
||
isOpen: boolean
|
||
searchText?: string
|
||
onChange?: (v: string) => void
|
||
onClose: () => void
|
||
onSearch: (v: string) => void
|
||
}
|
||
const SearchOverlay = memo<SearchOverlayProps>(function SearchOverlay({ isOpen, onChange, onClose, onSearch }) {
|
||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||
const [searchText, setSearchText] = useState('')
|
||
|
||
const handleTextChange = useCallback(
|
||
(text: string) => {
|
||
setSearchText(text)
|
||
onChange?.(text)
|
||
|
||
if (timerRef.current) {
|
||
clearTimeout(timerRef.current)
|
||
}
|
||
|
||
// 增加防抖时间到500ms,减少频繁搜索
|
||
timerRef.current = setTimeout(() => {
|
||
onSearch(text)
|
||
}, 500)
|
||
},
|
||
[onChange, onSearch],
|
||
)
|
||
|
||
const handleClose = useCallback(() => {
|
||
setSearchText('')
|
||
onSearch('')
|
||
onClose?.()
|
||
}, [onClose, onSearch])
|
||
|
||
if (!isOpen) return null
|
||
return (
|
||
<Block className="absolute inset-x-0 top-0 z-50 mt-[24px] flex-row items-center gap-[8px] px-[20px]">
|
||
<Block
|
||
className="flex-1 flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-[4px_4px_0px_#000]"
|
||
style={{ height: 48, transform: [{ skewX: '-6deg' }] }}
|
||
>
|
||
<Ionicons color="black" name="search" size={20} style={{ marginRight: 8 }} />
|
||
<TextInput
|
||
autoFocus
|
||
onChangeText={handleTextChange}
|
||
placeholder="搜索作品 / 用户..."
|
||
placeholderTextColor="#9CA3AF"
|
||
style={{
|
||
flex: 1,
|
||
fontSize: 14,
|
||
fontWeight: 'bold',
|
||
color: 'black',
|
||
}}
|
||
value={searchText}
|
||
/>
|
||
</Block>
|
||
<Block
|
||
className="items-center justify-center border-[3px] border-black bg-accent shadow-[4px_4px_0px_#000]"
|
||
onClick={handleClose}
|
||
style={{ width: 48, height: 48, transform: [{ skewX: '-6deg' }] }}
|
||
>
|
||
<Ionicons color="black" name="close" size={24} style={{ transform: [{ skewX: '6deg' }] }} />
|
||
</Block>
|
||
</Block>
|
||
)
|
||
})
|
||
|
||
type GooActionsProps = {
|
||
gooPoints: number
|
||
onAddGoo: () => void
|
||
onOpenSearch: () => void
|
||
}
|
||
const GooActions = observer<GooActionsProps>(function GooActions({ gooPoints, onAddGoo, onOpenSearch }) {
|
||
const { isPolling } = userBalanceStore
|
||
const isDev = __DEV__
|
||
const isAuthenticated = userStore.isAuthenticated
|
||
return (
|
||
<Block>
|
||
<Block className="mt-[12px] flex-row items-center gap-[8px]">
|
||
{!!isAuthenticated && (
|
||
<Block
|
||
className="flex-row items-center gap-[4px] rounded-full border-[3px] border-white bg-black px-[12px] py-[8px] shadow-[3px_3px_0px_rgba(0,0,0,0.5)]"
|
||
onClick={onAddGoo}
|
||
>
|
||
<Text className="text-[12px] font-black text-accent">{gooPoints}</Text>
|
||
<Ionicons color="#FFE500" name="add" size={12} style={{ fontWeight: 'bold' }} />
|
||
{/* 开发模式下显示轮询状态 */}
|
||
{isDev && isPolling && <Block className="ml-[4px] size-[6px] rounded-full bg-green-500" />}
|
||
</Block>
|
||
)}
|
||
|
||
<Block
|
||
className="size-[48px] items-center justify-center rounded-full border-[3px] border-black bg-white shadow-[4px_4px_0px_#000]"
|
||
onClick={onOpenSearch}
|
||
>
|
||
<Ionicons color="black" name="search" size={22} />
|
||
</Block>
|
||
</Block>
|
||
</Block>
|
||
)
|
||
})
|
||
|
||
type HeroCircleProps = {
|
||
selectedItem: MediaItem | null
|
||
onQuickGen: () => void
|
||
onOpenSearch: () => void
|
||
}
|
||
const HeroCircle = observer<HeroCircleProps>(function HeroCircle({ selectedItem, onQuickGen, onOpenSearch }) {
|
||
const isAuthenticated = userStore.isAuthenticated
|
||
const { balance } = userBalanceStore
|
||
const existItem = !!selectedItem?.url
|
||
return (
|
||
<Block className="relative z-10 items-center">
|
||
<Block className="relative mx-[12px] flex-row justify-between">
|
||
<Block className="flex-1">
|
||
<Block className="relative z-10 mt-[20px] flex size-[224px] rounded-full border-4 border-black shadow-deep-black">
|
||
{existItem && (
|
||
<Block className="relative size-full overflow-hidden rounded-full">
|
||
<VideoBox style={{ height: 216, width: 216, borderRadius: 108 }} url={selectedItem.url} />
|
||
</Block>
|
||
)}
|
||
|
||
{!existItem && (
|
||
<Block className="relative size-full overflow-hidden rounded-full bg-transparent">
|
||
<Text className="text-[14px] font-bold text-white/60">加载中...</Text>
|
||
</Block>
|
||
)}
|
||
|
||
<Block className="pointer-events-none absolute inset-0 rounded-full border-2 border-black/10" />
|
||
|
||
<Block className="absolute bottom-[24px] left-[8px] z-20 max-w-[100px] bg-black px-[12px] py-[4px] shadow-small-accent">
|
||
<Text className="font-900 -skew-x-12 text-[10px] text-white" ellipsizeMode="tail" numberOfLines={1}>
|
||
{selectedItem?.title}
|
||
</Text>
|
||
</Block>
|
||
|
||
<Block
|
||
className="absolute bottom-[16px] right-0 z-30 -skew-x-6 flex-row items-center gap-[4px] border-[3px] border-black bg-accent px-[16px] py-[6px] shadow-medium-black"
|
||
onClick={onQuickGen}
|
||
>
|
||
<Ionicons color="black" name="flash" size={14} />
|
||
<Text className="font-900 text-[12px] text-black">GET 同款</Text>
|
||
</Block>
|
||
</Block>
|
||
</Block>
|
||
<GooActions
|
||
gooPoints={balance}
|
||
onAddGoo={() => {
|
||
if (!isAuthenticated) {
|
||
router.push('/auth')
|
||
} else {
|
||
router.push('/pointList')
|
||
}
|
||
}}
|
||
onOpenSearch={onOpenSearch}
|
||
/>
|
||
</Block>
|
||
</Block>
|
||
)
|
||
})
|
||
|
||
type FilterSectionProps = {
|
||
activeTab: ActiveTab
|
||
onChange: (t: ActiveTab) => void
|
||
}
|
||
const FilterSection = memo<FilterSectionProps>(function FilterSection({ activeTab, onChange }) {
|
||
const tabs = useMemo(
|
||
() => [
|
||
{ label: '最热', state: '' as const },
|
||
{ label: '最新', state: 'new' as const },
|
||
{ label: '喜欢', state: 'like' as const },
|
||
],
|
||
[],
|
||
)
|
||
|
||
return (
|
||
<Block className="mt-[12px] flex-row items-end justify-between">
|
||
<Block className="flex-row gap-[8px]">
|
||
{tabs.map(({ label, state }) => {
|
||
const isActive = activeTab === state
|
||
return (
|
||
<Block
|
||
className={`border-2 border-black px-[16px] py-[4px] ${isActive ? 'bg-accent' : 'bg-white'}`}
|
||
key={state}
|
||
onClick={() => onChange(state)}
|
||
style={{
|
||
transform: [{ skewX: '-6deg' }],
|
||
}}
|
||
>
|
||
<Text className={`text-[10px] font-[900] ${isActive ? 'text-black' : 'text-gray-500'}`}>{label}</Text>
|
||
</Block>
|
||
)
|
||
})}
|
||
</Block>
|
||
</Block>
|
||
)
|
||
})
|
||
|
||
type GridItemProps = {
|
||
item: MediaItem
|
||
isSelected: boolean
|
||
itemWidth: number
|
||
onSelect: () => void
|
||
}
|
||
|
||
// 优化GridItem组件,减少不必要的重渲染
|
||
const GridItem = memo<GridItemProps>(
|
||
function GridItem({ item, isSelected, itemWidth, onSelect }) {
|
||
// console.log('item-------------', item);
|
||
|
||
return (
|
||
<Block className="relative mb-[12px]" onClick={onSelect}>
|
||
<Block
|
||
className={`relative overflow-hidden border-2 ${isSelected ? 'border-accent' : 'border-black'}`}
|
||
style={{
|
||
transform: [{ skewX: '-6deg' }],
|
||
height: itemWidth,
|
||
width: itemWidth,
|
||
}}
|
||
>
|
||
<VideoBox style={{ height: itemWidth, width: itemWidth }} url={item.url} />
|
||
|
||
{isSelected && <Block className="absolute inset-0 border-[3px] border-accent" />}
|
||
|
||
<Block
|
||
className={cn(
|
||
'absolute left-[0px] top-[0px] z-[20] border-b-[2px] border-r-[2px] border-black bg-black px-[6px] py-[2px]',
|
||
{
|
||
'bg-accent': isSelected,
|
||
},
|
||
)}
|
||
>
|
||
<Text
|
||
className={cn('font-900 text-[8px] text-white', {
|
||
'text-black': isSelected,
|
||
})}
|
||
>
|
||
{item.title}
|
||
</Text>
|
||
</Block>
|
||
|
||
<Block
|
||
className="absolute inset-x-0 bottom-0 z-20 items-end justify-between p-[4px] pt-[16px]"
|
||
style={{ transform: [{ skewX: '6deg' }] }}
|
||
>
|
||
<LinearGradient
|
||
colors={['rgba(0,0,0,0.8)', 'transparent']}
|
||
end={{ x: 0, y: 0 }}
|
||
start={{ x: 0, y: 1 }}
|
||
style={{
|
||
position: 'absolute',
|
||
left: 0,
|
||
right: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
}}
|
||
/>
|
||
|
||
<Block className="w-full flex-1 flex-row items-center justify-between px-[4px]">
|
||
<Block className="">
|
||
<Text className="font-900 max-w-[40px] -skew-x-6 text-[7px] text-white" numberOfLines={1}>
|
||
{item.authorName || '未知作者'}
|
||
</Text>
|
||
</Block>
|
||
<Block className="-skew-x-6 flex-row items-center gap-[2px]">
|
||
<Ionicons color="#FF0000" name="heart" size={10} />
|
||
<Text className="font-900 text-[8px] text-white">{item.likeCount}</Text>
|
||
</Block>
|
||
</Block>
|
||
</Block>
|
||
</Block>
|
||
</Block>
|
||
)
|
||
},
|
||
(prevProps, nextProps) => {
|
||
// 自定义比较函数,只在关键属性变化时才重渲染
|
||
return (
|
||
prevProps.item.id === nextProps.item.id &&
|
||
prevProps.isSelected === nextProps.isSelected &&
|
||
prevProps.itemWidth === nextProps.itemWidth &&
|
||
prevProps.item.likeCount === nextProps.item.likeCount
|
||
)
|
||
},
|
||
)
|
||
|
||
export default Index
|