feat: 优化数据请求逻辑,添加页面聚焦时重新请求数据的功能;重构相关代码

This commit is contained in:
康猛 2026-01-06 15:38:42 +08:00
parent 30d06355a3
commit a87bc6328c
3 changed files with 212 additions and 495 deletions

View File

@ -1,6 +1,5 @@
import { Ionicons } from '@expo/vector-icons'
import { Block, ConfirmModal, Text, Toast, VideoBox } from '@share/components'
import Img from '@share/components/Img'
import { FlashList } from '@shopify/flash-list'
import { LinearGradient } from 'expo-linear-gradient'
import { router, useFocusEffect } from 'expo-router'
@ -79,7 +78,7 @@ const Index = observer(function Index() {
isDeleted?: boolean
}
const transformTemplateToMediaItem = (template: TemplateData): MediaItem => {
const transformTemplateToMediaItem = useCallback((template: TemplateData): MediaItem => {
const isVideo = template.previewUrl?.includes('.mp4')
return {
id: template.id,
@ -89,7 +88,7 @@ const Index = observer(function Index() {
likeCount: template.likeCount || 0,
price: template.price || 2,
}
}
}, [])
/** ================= 统一请求函数 ================= */
@ -136,7 +135,54 @@ const Index = observer(function Index() {
setHasMore(false)
}
setAllItems((prev) => (mode === 'loadMore' ? [...prev, ...newItems] : newItems))
const updatedItems = mode === 'loadMore' ? [...allItems, ...newItems] : newItems
// 检测重复的 id 并打印
const checkDuplicateItems = (items: MediaItem[]) => {
const idMap = new Map<string, MediaItem[]>()
// 将items按id分组
items.forEach((item) => {
if (!idMap.has(item.id)) {
idMap.set(item.id, [])
}
idMap.get(item.id)!.push(item)
})
// 找出有重复id的项目
const duplicates: { id: string; items: MediaItem[] }[] = []
idMap.forEach((itemsWithSameId, id) => {
if (itemsWithSameId.length > 1) {
duplicates.push({ id, items: itemsWithSameId })
}
})
if (duplicates.length > 0) {
console.log('🚨 发现重复的items:')
duplicates.forEach(({ id, items }) => {
console.log(`ID: ${id} 重复了 ${items.length} 次:`)
items.forEach((item, index) => {
console.log(` [${index + 1}]`, {
id: item.id,
type: item.type,
url: item.url,
likeCount: item.likeCount,
price: item.price,
})
})
console.log('---')
})
} else {
console.log('✅ 没有发现重复的items')
}
return duplicates
}
// 检测并打印重复项
checkDuplicateItems(updatedItems)
setAllItems(updatedItems)
} catch (e) {
console.error('fetchList error:', e)
setHasMore(false)
@ -154,7 +200,7 @@ const Index = observer(function Index() {
/** ================= tab 切换(抽象后的重点) ================= */
const changeTab = (tab: ActiveTab) => {
const changeTab = useCallback((tab: ActiveTab) => {
queryRef.current = {
...queryRef.current,
tab,
@ -163,11 +209,11 @@ const Index = observer(function Index() {
setHasMore(true)
firstLoadDoneRef.current = false
fetchList('init')
}
}, [])
/** ================= 搜索 ================= */
const handleSearch = (text: string) => {
const handleSearch = useCallback((text: string) => {
queryRef.current = {
...queryRef.current,
search: text,
@ -176,18 +222,18 @@ const Index = observer(function Index() {
setHasMore(true)
firstLoadDoneRef.current = false
fetchList('refresh')
}
}, [])
/** ================= 刷新 / 加载更多 ================= */
const handleRefresh = () => {
const handleRefresh = useCallback(() => {
queryRef.current.page = 1
setHasMore(true)
firstLoadDoneRef.current = false
fetchList('refresh')
}
}, [])
const handleLoadMore = () => {
const handleLoadMore = useCallback(() => {
// 防止重复请求或首次渲染时误触发
if (loadingRef.current || !hasMore || refreshing || loadingMore) return
if (!firstLoadDoneRef.current) return
@ -196,7 +242,7 @@ const Index = observer(function Index() {
queryRef.current.page += 1
fetchList('loadMore')
console.log('handleLoadMore-------------')
}
}, [hasMore, refreshing, loadingMore])
/** ================= selectedItem 修正 ================= */
@ -262,17 +308,25 @@ const Index = observer(function Index() {
Toast.show({ title: '请先选择一个模板' })
return
}
if (balance < selectedItem.price) {
Toast.show({ title: '余额不足,请充值' })
// 显示加载状态并刷新余额
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()
}
await userBalanceStore.load(true) // 生成前刷新余额
// 如果余额仍不足,提示充值
if (balance < selectedItem.price) {
Toast.show({ title: '余额不足,请充值' })
return
}
Toast.showModal(
<ConfirmModal
content={
@ -284,21 +338,33 @@ const Index = observer(function Index() {
onCancel={Toast.hideModal}
onConfirm={async () => {
Toast.hideModal()
// 先进行乐观更新,扣减余额
userBalanceStore.deductBalance(selectedItem.price)
Toast.showLoading()
const { generationId, error } = await runTemplate({
templateId: selectedItem.id,
data: {},
originalUrl: selectedItem.url,
})
if (generationId && user?.id) {
// 生成成功后强制刷新余额以获取准确数据
userBalanceStore.load(true)
} else {
// 生成失败,恢复余额
try {
// 先进行乐观更新,扣减余额
userBalanceStore.deductBalance(selectedItem.price)
const { generationId, error } = await runTemplate({
templateId: selectedItem.id,
data: {},
originalUrl: selectedItem.url,
})
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: error?.message || '生成失败' })
Toast.show({ title: '网络异常,请重试' })
} finally {
Toast.hideLoading()
}
}}
/>,
@ -329,6 +395,21 @@ const Index = observer(function Index() {
}
}
// 缓存渲染函数以提升性能
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]">
@ -368,14 +449,8 @@ const Index = observer(function Index() {
drawDistance={1200}
onEndReachedThreshold={0.3}
refreshControl={<RefreshControl colors={['#FFE500']} refreshing={refreshing} onRefresh={handleRefresh} />}
renderItem={({ item }) => (
<GridItem
isSelected={selectedItem?.id === item.id}
item={item}
itemWidth={ITEM_WIDTH}
onSelect={() => setSelectedItem(item)}
/>
)}
renderItem={renderItem}
keyExtractor={keyExtractor}
ListEmptyComponent={renderListEmpty}
showsVerticalScrollIndicator={false}
data={allItems}
@ -398,33 +473,29 @@ type SearchOverlayProps = {
const SearchOverlay = memo<SearchOverlayProps>(function SearchOverlay({ isOpen, onChange, onClose, onSearch }) {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [searchText, setSearchText] = useState('')
const handleTextChange = (text: string) => {
setSearchText(text)
onChange && onChange(text)
if (timerRef.current) {
clearTimeout(timerRef.current)
}
const handleTextChange = useCallback(
(text: string) => {
setSearchText(text)
onChange?.(text)
timerRef.current = setTimeout(() => {
onSearch(text)
}, 2000)
}
useEffect(() => {
// 删除定时器
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}
}, [])
const handleClose = () => {
// 增加防抖时间到500ms减少频繁搜索
timerRef.current = setTimeout(() => {
onSearch(text)
}, 500)
},
[onChange, onSearch],
)
const handleClose = useCallback(() => {
setSearchText('')
onSearch('')
onClose && onClose()
}
onClose?.()
}, [onClose, onSearch])
if (!isOpen) return null
return (
@ -516,11 +587,7 @@ const HeroCircle = memo<HeroCircleProps>(function HeroCircle({ selectedItem, onQ
<Block className="relative mx-[12px] flex-row justify-between">
<Block className="relative z-10 mt-[20px] size-[224px] rounded-full border-4 border-black bg-white shadow-deep-black">
<Block className="relative size-full overflow-hidden rounded-full bg-black">
{selectedItem.type === 'video' ? (
<VideoBox style={{ height: 216, width: 216, borderRadius: 108 }} url={selectedItem.url} />
) : (
<Img className="size-full" src={selectedItem.url} />
)}
<VideoBox style={{ height: 216, width: 216, borderRadius: 108 }} url={selectedItem.url} />
<Block className="absolute inset-0">
<LinearGradient
@ -605,72 +672,85 @@ type GridItemProps = {
itemWidth: number
onSelect: () => void
}
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" />}
// 优化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={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,
},
)}
className={`relative overflow-hidden border-2 ${isSelected ? 'border-accent' : 'border-black'}`}
style={{
transform: [{ skewX: '-6deg' }],
height: itemWidth,
width: itemWidth,
}}
>
<Text
className={cn('font-900 text-[8px] text-white', {
'text-black': isSelected,
})}
<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,
},
)}
>
{item.id}
</Text>
</Block>
<Text
className={cn('font-900 text-[8px] text-white', {
'text-black': isSelected,
})}
>
{item.id}
</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="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.id}
</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 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.id}
</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>
</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

View File

@ -1,373 +0,0 @@
import { Ionicons } from '@expo/vector-icons'
import { Block, ConfirmModal, Text, Toast, VideoBox } from '@share/components'
import Img from '@share/components/Img'
import { LinearGradient } from 'expo-linear-gradient'
import { router } from 'expo-router'
import { observer } from 'mobx-react-lite'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Dimensions, ScrollView } from 'react-native'
import { useTemplateActions } from '@/hooks/actions/use-template-actions'
import { type TemplateGeneration, useTemplateGenerations } from '@/hooks/data/use-template-generations'
import { userBalanceStore, userStore } from '@/stores'
const BACKGROUND_VIDEOS = [
'https://cdn.roasmax.cn/material/b46f380532e14cf58dd350dbacc7c34a.mp4',
'https://cdn.roasmax.cn/material/992e6c5d940c42feb71c27e556b754c0.mp4',
'https://cdn.roasmax.cn/material/e4947477843f4067be7c37569a33d17b.mp4',
]
function isVideoUrl(url: string) {
return url.endsWith('.mp4')
}
const My = observer(function My() {
// 从MobX Store获取用户信息
const { user, isLoading: authLoading, signOut } = userStore
const { data: generationsData, loading: generationsLoading, load: loadGenerations } = useTemplateGenerations()
const { batchDeleteGenerations, loading: deleteLoading } = useTemplateActions()
// 使用MobX Store中的余额信息
const { balance } = userBalanceStore
const [activeFilter, setActiveFilter] = useState<'my_gen' | 'my_album'>('my_gen')
const [bgVideo] = useState(() => BACKGROUND_VIDEOS[Math.floor(Math.random() * BACKGROUND_VIDEOS.length)])
const [isSelectionMode, setIsSelectionMode] = useState(false)
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
useEffect(() => {
if (user?.id) {
loadGenerations({ userId: user.id })
userBalanceStore.load(true) // 加载用户余额
}
}, [user?.id])
const generations = useMemo(() => generationsData?.data || [], [generationsData])
const filteredPosts =
activeFilter === 'my_gen' ? generations : generations.filter((g: TemplateGeneration) => g.status === 'completed')
const toggleSelectionMode = useCallback(() => {
setIsSelectionMode((v) => !v)
setSelectedIds(new Set())
}, [])
const handleItemClick = useCallback(
(generation: any) => {
if (isSelectionMode) {
setSelectedIds((prev) => {
const next = new Set(prev)
if (next.has(generation.id)) next.delete(generation.id)
else next.add(generation.id)
return next
})
}
},
[isSelectionMode],
)
const handleDelete = useCallback(async () => {
if (selectedIds.size === 0) return
const { success } = await batchDeleteGenerations(Array.from(selectedIds))
if (success && user?.id) {
await loadGenerations({ userId: user.id })
setSelectedIds(new Set())
setIsSelectionMode(false)
}
}, [selectedIds, batchDeleteGenerations, user?.id, loadGenerations])
const selectAll = useCallback(() => {
if (selectedIds.size === filteredPosts.length) setSelectedIds(new Set())
else setSelectedIds(new Set(filteredPosts.map((p: TemplateGeneration) => p.id)))
}, [selectedIds.size, filteredPosts])
const handleLogout = useCallback(() => {
Toast.showModal(
<ConfirmModal
title="确认退出登录?"
badge="LOGOUT"
content="退出登录后需要重新登录才能使用完整功能"
confirmText="确认退出"
cancelText="取消"
onConfirm={async () => {
Toast.hideModal()
await signOut()
router.replace('/auth')
}}
onCancel={() => Toast.hideModal()}
/>,
{},
)
}, [signOut])
const { width: screenWidth } = Dimensions.get('window')
const itemWidth = Math.floor((screenWidth - 24 - 12 * 2) / 3)
const renderBanner = useCallback(
() => (
<Block className="absolute inset-0 z-0 overflow-hidden">
<VideoBox url={bgVideo} style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, opacity: 0.6 }} />
<Block className="absolute inset-0 bg-black/10" />
</Block>
),
[bgVideo],
)
const renderHeaderCard = useCallback(() => {
const username = user?.name || user?.email || 'Guest'
const avatarUrl =
user?.image || 'https://image.pollinations.ai/prompt/cool%20anime%20boy%20avatar%20hoodie?seed=123&nologo=true'
const uid = user?.id?.slice(-6) || '000000'
const generationCount = generations.length
const completedCount = generations.filter((g: TemplateGeneration) => g.status === 'completed').length
return (
<Block className="relative mb-[24px] mt-[16px] px-[12px]">
<Block className="relative overflow-hidden border-4 border-black bg-white p-[16px] shadow-deep-black">
<Block style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, opacity: 0.1 }} />
<Block className="relative z-10 flex-row items-center gap-[16px]">
<Block className="size-[96px] border-[3px] border-black bg-accent p-[4px] shadow-soft-black-10">
<Img src={avatarUrl} className="size-full border border-black" />
</Block>
<Block className="flex-1">
<Block className="mb-[4px] flex-row items-center justify-between">
<Text className="font-900 -skew-x-6 text-[24px] tracking-[-0.5px] text-black" numberOfLines={1}>
{username}
</Text>
<Block onClick={handleLogout} className="border-2 border-black bg-black p-[8px] shadow-medium-gray">
<Ionicons name="log-out-outline" size={18} color="white" />
</Block>
</Block>
<Block className="flex-row items-center gap-[8px]">
<Text className="font-900 border border-black bg-black px-[8px] py-[2px] text-[10px] text-white">
UID: {uid}
</Text>
<Text className="font-900 border-2 border-black bg-accent px-[8px] py-[2px] text-[10px] text-black shadow-small-accent">
CREDITS: {balance}
</Text>
</Block>
<Block className="mt-[12px] flex-row items-center gap-[16px] border-t-2 border-black pt-[8px]">
<Block>
<Text className="font-700 text-[10px] text-gray-500">TOTAL</Text>
<Text className="font-900 text-[14px] text-black">{generationCount}</Text>
</Block>
<Block>
<Text className="font-700 text-[10px] text-gray-500">COMPLETED</Text>
<Text className="font-900 text-[14px] text-black">{completedCount}</Text>
</Block>
<Block>
<Text className="font-700 text-[10px] text-gray-500">CREDITS</Text>
<Text className="font-900 text-[14px] text-black">{balance}</Text>
</Block>
</Block>
</Block>
</Block>
{balance > 100 && (
<Block className="absolute right-[-32px] top-px z-20 rotate-45 border-y-2 border-black bg-[#e61e25] px-[40px] py-[4px]">
<Text className="font-900 text-[12px] text-white">PRO</Text>
</Block>
)}
</Block>
</Block>
)
}, [user, balance, generations])
const renderActions = useCallback(
() => (
<Block className="mb-[24px] flex-row gap-[12px] px-[12px]">
{[
{ label: 'SHOP', color: '#4ADE80', icon: 'bag-outline' as const },
{ label: 'SYNC', color: '#FFE500', icon: 'watch-outline' as const },
{ label: 'PAY', color: '#e61e25', icon: 'card-outline' as const },
].map(({ label, color, icon }) => (
<Block
key={label}
className="relative flex-1 items-center justify-center border-[3px] border-black bg-white"
style={{ height: 56 }}
>
<Ionicons name={icon} size={20} color="black" />
<Text className="font-900 text-[10px] text-black">{label}</Text>
</Block>
))}
</Block>
),
[],
)
const renderFilters = useCallback(
() => (
<Block className="mb-[12px] flex-row items-center justify-between px-[12px]">
<Block className="flex-row gap-[8px]">
{['我的生成', '我的专辑'].map((label) => {
const target = label === '我的生成' ? 'my_gen' : 'my_album'
const isActive = activeFilter === (target as 'my_gen' | 'my_album')
return (
<Block
key={label}
onClick={() => setActiveFilter(target as 'my_gen' | 'my_album')}
className={`border-2 border-black px-[16px] py-[4px] ${isActive ? 'bg-black' : 'bg-white'} -skew-x-12`}
>
<Text className={`font-900 text-[10px] ${isActive ? 'text-accent' : 'text-black'} skew-x-12`}>
{label}
</Text>
</Block>
)
})}
</Block>
<Block
onClick={toggleSelectionMode}
className={`border-2 border-black px-[16px] py-[6px] shadow-medium-black ${isSelectionMode ? 'bg-[#e61e25] text-white' : 'bg-accent text-black'}`}
>
<Text className="font-900 text-[12px]">{isSelectionMode ? '取消' : '管理'}</Text>
</Block>
</Block>
),
[activeFilter, isSelectionMode, toggleSelectionMode],
)
const renderGrid = useCallback(() => {
if (filteredPosts.length === 0) {
return (
<Block className="items-center justify-center px-[12px] py-[80px]">
<Block className="items-center gap-[16px] border-[3px] border-black bg-white p-[32px] shadow-large-black">
<Ionicons name="images-outline" size={64} color="#cccccc" />
<Text className="font-900 text-[16px] text-gray-500">
{activeFilter === 'my_gen' ? '暂无生成作品' : '暂无收藏'}
</Text>
</Block>
</Block>
)
}
return (
<Block className="px-[12px]">
<Block className="flex-row flex-wrap gap-[12px]">
{filteredPosts.map((generation: TemplateGeneration) => {
const isSelected = selectedIds.has(generation.id)
const imageUrl = generation.resultUrl?.[0] || generation.originalUrl || ''
const statusRank = generation.status === 'completed' ? 'S' : generation.status === 'processing' ? 'A' : 'B'
return (
<Block key={generation.id} onClick={() => handleItemClick(generation)} className="relative">
<Block
className={`relative overflow-hidden border-2 ${isSelected ? 'shadow-[0px_0px_0px_4px_#FFE500]' : 'shadow-hard-black'} ${isSelected ? 'border-accent' : 'border-black'}`}
style={{ transform: [{ skewX: '-6deg' }], height: itemWidth, width: itemWidth }}
>
<Block style={{ transform: [{ skewX: '6deg' }], height: itemWidth, width: itemWidth }}>
{imageUrl ? (
isVideoUrl(imageUrl) ? (
<VideoBox url={`${imageUrl}#t=0.1`} style={{ height: itemWidth, width: itemWidth }} />
) : (
<Img src={imageUrl} className="size-full" />
)
) : (
<Block className="size-full items-center justify-center bg-gray-200">
<Ionicons name="image-outline" size={32} color="#999" />
</Block>
)}
</Block>
{isSelected && <Block className="absolute inset-0 border-[3px] border-accent" />}
{isSelectionMode && (
<Block
className="absolute inset-0 z-30 items-center justify-center"
style={{ transform: [{ skewX: '6deg' }] }}
>
<Block
className={`size-[32px] items-center justify-center rounded-full border-[3px] border-black ${isSelected ? 'bg-accent' : 'bg-white/50'}`}
>
<Ionicons name="checkmark" size={20} color="black" />
</Block>
</Block>
)}
{!isSelectionMode && (
<Block className="absolute left-0 top-0 z-20 border-b-2 border-r-2 border-black bg-accent px-[6px] py-[2px]">
<Text className="font-900 text-[8px] text-black">{statusRank}</Text>
</Block>
)}
{!isSelectionMode && (
<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']}
start={{ x: 0, y: 1 }}
end={{ x: 0, y: 0 }}
style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 }}
/>
<Text className="font-900 max-w-[40px] -skew-x-6 text-[7px] text-white" numberOfLines={1}>
{generation.type}
</Text>
<Block className="-skew-x-6 flex-row items-center gap-[2px]">
<Ionicons name="time-outline" size={10} color="#FFE500" />
<Text className="font-900 text-[8px] text-white">
{new Date(generation.createdAt).toLocaleDateString()}
</Text>
</Block>
</Block>
)}
</Block>
</Block>
)
})}
</Block>
<Block className="h-[200px] w-full" />
</Block>
)
}, [filteredPosts, selectedIds, isSelectionMode, handleItemClick, activeFilter, itemWidth])
const renderSelection = useCallback(() => {
if (!isSelectionMode) return null
return (
<Block className="absolute inset-x-[16px] bottom-[96px] z-50">
<Block className="-skew-x-3 flex-row items-center justify-between border-[3px] border-black bg-white p-[12px] shadow-large-black">
<Block className="skew-x-3 flex-row items-center gap-[12px] pl-[8px]">
<Text className="font-900 text-[14px]">: {selectedIds.size}</Text>
<Block onClick={selectAll} className="text-[12px] underline">
<Text className="font-700 text-[12px]"></Text>
</Block>
</Block>
<Block
onClick={handleDelete}
className={`font-900 skew-x-3 flex-row items-center gap-[8px] border-2 border-black px-[16px] py-[8px] text-[14px] shadow-medium-black${selectedIds.size > 0 && !deleteLoading ? 'bg-[#e61e25]' : 'bg-gray-200'}`}
>
{deleteLoading ? (
<Ionicons name="hourglass-outline" size={16} color="white" />
) : (
<Ionicons name="trash-outline" size={16} color="white" />
)}
<Text className="font-900 text-[14px] text-white">{deleteLoading ? '删除中...' : '删除'}</Text>
</Block>
</Block>
</Block>
)
}, [isSelectionMode, selectedIds.size, selectAll, handleDelete, deleteLoading])
if (authLoading || generationsLoading) {
return (
<Block className="relative flex-1 items-center justify-center bg-black">
{renderBanner()}
<Block className="relative z-10 items-center gap-[16px] border-[3px] border-accent bg-white p-[32px] shadow-large-accent">
<Ionicons name="hourglass-outline" size={48} color="#FFE500" />
<Text className="font-900 text-[16px] text-black">...</Text>
</Block>
</Block>
)
}
return (
<Block className="relative flex-1 bg-black text-white">
{renderBanner()}
<ScrollView className="relative z-10 flex-1">
{renderHeaderCard()}
{renderActions()}
{renderFilters()}
{renderGrid()}
</ScrollView>
{renderSelection()}
</Block>
)
})
export default My

View File

@ -3,7 +3,7 @@ import { Block, ConfirmModal, Text, Toast, VideoBox } from '@share/components'
import Img from '@share/components/Img'
import { FlashList } from '@shopify/flash-list'
import * as ImagePicker from 'expo-image-picker'
import { router } from 'expo-router'
import { router, useFocusEffect } from 'expo-router'
import { observer } from 'mobx-react-lite'
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { ActivityIndicator, RefreshControl, ScrollView, View } from 'react-native'
@ -68,6 +68,16 @@ const Sync = observer(() => {
}
}, [user?.id, loadGenerations])
// 页面聚焦时重新请求数据
useFocusEffect(
useCallback(() => {
if (user?.id) {
console.log('页面聚焦,重新请求数据')
loadGenerations()
}
}, [user?.id, loadGenerations]),
)
// 将生成记录转换为 posts 格式
const posts = useMemo(() => {
const generations = generationsData?.data || []