This commit is contained in:
imeepos
2026-01-29 16:20:01 +08:00
parent 7c3b3c066c
commit 1ccdc23355
6 changed files with 315 additions and 229 deletions

View File

@@ -155,12 +155,10 @@ export default function MessageScreen() {
// Render message item
const renderMessageItem = useCallback(({ item }: { item: Message }) => (
<SwipeToDelete onDelete={() => handleDeleteMessage(item.id)}>
<MessageCard
message={item}
onPress={handleMessagePress}
/>
</SwipeToDelete>
<MessageCard
message={item}
onPress={handleMessagePress}
/>
), [handleDeleteMessage, handleMessagePress])
// Render list footer
@@ -288,6 +286,7 @@ const styles = StyleSheet.create({
listContent: {
paddingHorizontal: 4,
paddingTop: 12,
paddingBottom: 100,
},
emptyListContent: {
flex: 1,

View File

@@ -32,7 +32,6 @@ import { MySkeleton } from '@/components/skeleton/MySkeleton'
import { TabNavigation } from '@/components/blocks/home/TabNavigation'
import { useUserBalance } from '@/hooks/use-user-balance'
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'
const { width: screenWidth } = Dimensions.get('window')
const GALLERY_GAP = 2
@@ -136,9 +135,13 @@ export default function My() {
// 下拉刷新状态
const [refreshing, setRefreshing] = useState(false)
const [favoritesRefreshing, setFavoritesRefreshing] = useState(false)
const [likesRefreshing, setLikesRefreshing] = useState(false)
// 防止重复触发加载更多
const isLoadingMoreRef = useRef(false)
const isFavoritesLoadingMoreRef = useRef(false)
const isLikesLoadingMoreRef = useRef(false)
// 下拉刷新处理
const onRefresh = useCallback(async () => {
@@ -150,6 +153,26 @@ export default function My() {
}
}, [refetch])
// 收藏Tab下拉刷新处理
const onFavoritesRefresh = useCallback(async () => {
setFavoritesRefreshing(true)
try {
await favoritesRefetch()
} finally {
setFavoritesRefreshing(false)
}
}, [favoritesRefetch])
// 点赞Tab下拉刷新处理
const onLikesRefresh = useCallback(async () => {
setLikesRefreshing(true)
try {
await likesRefetch()
} finally {
setLikesRefreshing(false)
}
}, [likesRefetch])
// 加载更多处理
const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent
@@ -173,6 +196,40 @@ export default function My() {
}
}, [loadingMore, hasMore, loadMore, loading])
// 收藏Tab加载更多处理
const handleFavoritesScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent
const paddingToBottom = 200
const isCloseToBottom =
layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom
if (isCloseToBottom && !favoritesLoadingMore && favoritesHasMore && !favoritesLoading && !isFavoritesLoadingMoreRef.current) {
console.log('🔄 收藏Tab触发加载更多')
isFavoritesLoadingMoreRef.current = true
favoritesLoadMore().finally(() => {
isFavoritesLoadingMoreRef.current = false
})
}
}, [favoritesLoadingMore, favoritesHasMore, favoritesLoadMore, favoritesLoading])
// 点赞Tab加载更多处理
const handleLikesScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent
const paddingToBottom = 200
const isCloseToBottom =
layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom
if (isCloseToBottom && !likesLoadingMore && likesHasMore && !likesLoading && !isLikesLoadingMoreRef.current) {
console.log('🔄 点赞Tab触发加载更多')
isLikesLoadingMoreRef.current = true
likesLoadMore().finally(() => {
isLikesLoadingMoreRef.current = false
})
}
}, [likesLoadingMore, likesHasMore, likesLoadMore, likesLoading])
// 处理设置菜单选择
const handleSettingsSelect = async (value: string) => {
if (value === 'changePassword') {
@@ -231,81 +288,169 @@ export default function My() {
]
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<KeyboardAwareScrollView bottomOffset={50}>
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
{/* 顶部积分与设置 */}
<View style={styles.topBar}>
<Pressable
style={styles.pointsPill}
onPress={() => router.push('/membership' as any)}
>
<PointsIcon />
<Text style={styles.pointsPillText}>{balance}</Text>
</Pressable>
<Dropdown
options={settingsOptions}
onSelect={(value) => handleSettingsSelect(value)}
renderTrigger={(selectedOption, isOpen, toggle) => (
<Pressable onPress={toggle}>
<SettingsIcon />
</Pressable>
)}
dropdownStyle={{
minWidth: 160,
right: 10,
backgroundColor: '#2A2A2A80',
}}
/>
</View>
{/* 个人信息区 */}
<View style={styles.profileSection}>
<Image
source={session?.user?.image ? { uri: session.user.image } : require('@/assets/images/icon.png')}
style={styles.avatar}
contentFit="cover"
/>
<View style={styles.profileInfo}>
<Text style={styles.profileName}>{profileName}</Text>
<Text style={styles.profileSubTitle}>ID {session?.user?.id?.slice(0, 8) || '--------'}</Text>
</View>
<Pressable
style={styles.editButton}
onPress={() => {
console.log('[my.tsx] Edit Profile button pressed')
setEditDrawerVisible(true)
}}
>
<Text style={styles.editButtonText}>{t('my.editProfile')}</Text>
</Pressable>
</View>
{/* Tab 导航 */}
<TabNavigation
tabs={tabs}
activeIndex={activeTabIndex}
onTabPress={handleTabPress}
{/* 顶部积分与设置 */}
<View style={styles.topBar}>
<Pressable
style={styles.pointsPill}
onPress={() => router.push('/membership' as any)}
>
<PointsIcon />
<Text style={styles.pointsPillText}>{balance}</Text>
</Pressable>
<Dropdown
options={settingsOptions}
onSelect={(value) => handleSettingsSelect(value)}
renderTrigger={(selectedOption, isOpen, toggle) => (
<Pressable onPress={toggle}>
<SettingsIcon />
</Pressable>
)}
dropdownStyle={{
minWidth: 160,
right: 10,
backgroundColor: '#2A2A2A80',
}}
/>
</View>
{/* 作品九宫格 */}
{activeTab === 'works' && loading ? (
{/* 个人信息区 */}
<View style={styles.profileSection}>
<Image
source={session?.user?.image ? { uri: session.user.image } : require('@/assets/images/icon.png')}
style={styles.avatar}
contentFit="cover"
/>
<View style={styles.profileInfo}>
<Text style={styles.profileName}>{profileName}</Text>
<Text style={styles.profileSubTitle}>ID {session?.user?.id?.slice(0, 8) || '--------'}</Text>
</View>
<Pressable
style={styles.editButton}
onPress={() => {
console.log('[my.tsx] Edit Profile button pressed')
setEditDrawerVisible(true)
}}
>
<Text style={styles.editButtonText}>{t('my.editProfile')}</Text>
</Pressable>
</View>
{/* Tab 导航 */}
<TabNavigation
tabs={tabs}
activeIndex={activeTabIndex}
onTabPress={handleTabPress}
/>
{/* 作品九宫格 */}
{activeTab === 'works' && loading ? (
<MySkeleton />
) : activeTab === 'works' ? (
<ScrollView
testID="my-scroll-view"
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
onScroll={handleScroll}
scrollEventThrottle={16}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#9966FF"
colors={['#9966FF', '#FF6699', '#FF9966']}
progressBackgroundColor="#1C1E22"
progressViewOffset={10}
/>
}
>
<View style={styles.galleryGrid}>
{generations.map((item, index) => (
<Pressable
key={item.id}
style={[
styles.galleryItem,
index % 3 !== 2 && styles.galleryItemMarginRight,
styles.galleryItemMarginBottom,
]}
onPress={() => {
if (item.status === 'completed') {
router.push({
pathname: '/generationRecord' as any,
params: { id: item.id },
})
}
}}
disabled={item.status !== 'completed'}
>
<Image
source={getCoverUrl(item) ? { uri: getCoverUrl(item) } : require('@/assets/images/membership.png')}
style={styles.galleryImage}
contentFit="cover"
/>
{/* 遮罩:非完成状态 */}
{item.status !== 'completed' && (
<View style={styles.generatingOverlay} />
)}
{/* 数量角标:已完成且有结果 */}
{item.status === 'completed' && (item.resultUrl?.length || 0) > 0 && (
<View style={styles.counterBadge}>
<Text style={styles.counterText}>
{item.resultUrl?.length || 1}
</Text>
</View>
)}
{/* 状态角标 */}
{item.status === 'running' && (
<View style={styles.generatingBadge}>
<Text style={styles.generatingBadgeText}>{t('my.generating')}</Text>
</View>
)}
{item.status === 'pending' && (
<View style={styles.generatingBadge}>
<Text style={styles.generatingBadgeText}>{t('my.queuing')}</Text>
</View>
)}
</Pressable>
))}
{/* 加载更多指示器 */}
{loadingMore && (
<View style={styles.loadingMoreContainer}>
<ActivityIndicator size="small" color="#9966FF" />
</View>
)}
{/* 空状态提示 */}
{!loading && generations.length === 0 && (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}>
{t('my.noWorks')}
</Text>
</View>
)}
</View>
</ScrollView>
) : activeTab === 'favorites' && favoritesLoading ? (
<MySkeleton />
) : activeTab === 'works' ? (
) : activeTab === 'favorites' ? (
<ScrollView
testID="my-scroll-view"
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
onScroll={handleScroll}
onScroll={handleFavoritesScroll}
scrollEventThrottle={16}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
refreshing={favoritesRefreshing}
onRefresh={onFavoritesRefresh}
tintColor="#9966FF"
colors={['#9966FF', '#FF6699', '#FF9966']}
progressBackgroundColor="#1C1E22"
@@ -314,107 +459,37 @@ export default function My() {
}
>
<View style={styles.galleryGrid}>
{generations.map((item, index) => (
<Pressable
key={item.id}
style={[
styles.galleryItem,
index % 3 !== 2 && styles.galleryItemMarginRight,
styles.galleryItemMarginBottom,
]}
onPress={() => {
if (item.status === 'completed') {
router.push({
pathname: '/generationRecord' as any,
params: { id: item.id },
})
}
}}
disabled={item.status !== 'completed'}
>
<Image
source={getCoverUrl(item) ? { uri: getCoverUrl(item) } : require('@/assets/images/membership.png')}
style={styles.galleryImage}
contentFit="cover"
/>
{/* 遮罩:非完成状态 */}
{item.status !== 'completed' && (
<View style={styles.generatingOverlay} />
)}
{/* 数量角标:已完成且有结果 */}
{item.status === 'completed' && (item.resultUrl?.length || 0) > 0 && (
<View style={styles.counterBadge}>
<Text style={styles.counterText}>
{item.resultUrl?.length || 1}
</Text>
</View>
)}
{/* 状态角标 */}
{item.status === 'running' && (
<View style={styles.generatingBadge}>
<Text style={styles.generatingBadgeText}>{t('my.generating')}</Text>
</View>
)}
{item.status === 'pending' && (
<View style={styles.generatingBadge}>
<Text style={styles.generatingBadgeText}>{t('my.queuing')}</Text>
</View>
)}
</Pressable>
))}
{/* 加载更多指示器 */}
{loadingMore && (
<View style={styles.loadingMoreContainer}>
<ActivityIndicator size="small" color="#9966FF" />
</View>
)}
{/* 空状态提示 */}
{!loading && generations.length === 0 && (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}>
{t('my.noWorks')}
</Text>
</View>
)}
</View>
</ScrollView>
) : activeTab === 'favorites' && favoritesLoading ? (
<MySkeleton />
) : activeTab === 'favorites' ? (
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.galleryGrid}>
{favorites.length === 0 ? (
{favorites.length === 0 && !favoritesLoading ? (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}>
{t('my.noFavorites')}
</Text>
</View>
) : (
favorites.map((item, index) => (
<Pressable
key={item.id}
style={[
styles.galleryItem,
index % 3 !== 2 && styles.galleryItemMarginRight,
styles.galleryItemMarginBottom,
]}
>
<Image
source={item.template?.coverImageUrl ? { uri: item.template.coverImageUrl } : require('@/assets/images/membership.png')}
style={styles.galleryImage}
contentFit="cover"
/>
</Pressable>
))
<>
{favorites.map((item, index) => (
<Pressable
key={item.id}
style={[
styles.galleryItem,
index % 3 !== 2 && styles.galleryItemMarginRight,
styles.galleryItemMarginBottom,
]}
>
<Image
source={item.template?.coverImageUrl ? { uri: item.template.coverImageUrl } : require('@/assets/images/membership.png')}
style={styles.galleryImage}
contentFit="cover"
/>
</Pressable>
))}
{/* 加载更多指示器 */}
{favoritesLoadingMore && (
<View style={styles.loadingMoreContainer}>
<ActivityIndicator size="small" color="#9966FF" />
</View>
)}
</>
)}
</View>
</ScrollView>
@@ -425,48 +500,66 @@ export default function My() {
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
onScroll={handleLikesScroll}
scrollEventThrottle={16}
refreshControl={
<RefreshControl
refreshing={likesRefreshing}
onRefresh={onLikesRefresh}
tintColor="#9966FF"
colors={['#9966FF', '#FF6699', '#FF9966']}
progressBackgroundColor="#1C1E22"
progressViewOffset={10}
/>
}
>
<View style={styles.galleryGrid}>
{likes.length === 0 ? (
{likes.length === 0 && !likesLoading ? (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}>
{t('my.noLikes')}
</Text>
</View>
) : (
likes.map((item, index) => (
<Pressable
key={item.id}
style={[
styles.galleryItem,
index % 3 !== 2 && styles.galleryItemMarginRight,
styles.galleryItemMarginBottom,
]}
>
<Image
source={item.template?.coverImageUrl ? { uri: item.template.coverImageUrl } : require('@/assets/images/membership.png')}
style={styles.galleryImage}
contentFit="cover"
/>
</Pressable>
))
<>
{likes.map((item, index) => (
<Pressable
key={item.id}
style={[
styles.galleryItem,
index % 3 !== 2 && styles.galleryItemMarginRight,
styles.galleryItemMarginBottom,
]}
>
<Image
source={item.template?.coverImageUrl ? { uri: item.template.coverImageUrl } : require('@/assets/images/membership.png')}
style={styles.galleryImage}
contentFit="cover"
/>
</Pressable>
))}
{/* 加载更多指示器 */}
{likesLoadingMore && (
<View style={styles.loadingMoreContainer}>
<ActivityIndicator size="small" color="#9966FF" />
</View>
)}
</>
)}
</View>
</ScrollView>
) : null}
{/* 编辑资料抽屉 */}
<EditProfileDrawer
visible={editDrawerVisible}
onClose={() => setEditDrawerVisible(false)}
initialName={profileName}
initialAvatar={session?.user?.image}
onSave={(data) => {
setProfileName(data.name)
}}
/>
</KeyboardAwareScrollView>
{/* 编辑资料抽屉 */}
<EditProfileDrawer
visible={editDrawerVisible}
onClose={() => setEditDrawerVisible(false)}
initialName={profileName}
initialAvatar={session?.user?.image}
onSave={(data) => {
setProfileName(data.name)
}}
/>
</SafeAreaView>
)
}
@@ -506,7 +599,7 @@ const styles = StyleSheet.create({
scrollContent: {
backgroundColor: '#090A0B',
paddingHorizontal: GALLERY_HORIZONTAL_PADDING,
paddingBottom: 20,
paddingBottom: 100,
},
profileSection: {
flexDirection: 'row',

View File

@@ -112,9 +112,6 @@ export const MessageCard: React.FC<MessageCardProps> = ({
onDelete,
}) => {
const config = getMessageConfig(message.data?.subType)
const icon = getMessageIcon(message.data?.subType)
const showAvatar = config.showAvatar && message.data?.likerAvatar
const showPreview = config.showPreview && message.data?.webpPreviewUrl
const showQuote = config.showQuote && message.data?.originalComment
const handlePress = () => {
@@ -148,15 +145,6 @@ export const MessageCard: React.FC<MessageCardProps> = ({
{message.content}
</Text>
{/* Preview Image */}
{showPreview && (
<Image
testID="preview-image"
source={{ uri: message.data?.webpPreviewUrl }}
style={styles.previewImage}
/>
)}
{/* Quote */}
{showQuote && (
<View testID="quote-container" style={styles.quoteContainer}>

View File

@@ -25,8 +25,8 @@ export const useMessages = (initialParams?: ListMessagesParams) => {
const [refreshing, setRefreshing] = useState(false)
const [error, setError] = useState<ApiError | null>(null)
const [data, setData] = useState<ListMessagesResult>()
const [hasMore, setHasMore] = useState(true)
const currentPageRef = useRef(1)
const hasMoreRef = useRef(true)
const execute = useCallback(async (params?: ListMessagesParams) => {
setLoading(true)
@@ -53,14 +53,15 @@ export const useMessages = (initialParams?: ListMessagesParams) => {
const currentPage = requestParams.page || 1
const totalPages = data?.totalPages || 1
hasMoreRef.current = currentPage < totalPages
const hasMoreValue = currentPage < totalPages
setHasMore(hasMoreValue)
setData(data)
setLoading(false)
return { data, error: null }
}, [initialParams])
const loadMore = useCallback(async () => {
if (loadingMore || loading || !hasMoreRef.current) return { data: undefined, error: null }
if (loadingMore || loading || !hasMore) return { data: undefined, error: null }
setLoadingMore(true)
const nextPage = currentPageRef.current + 1
@@ -83,7 +84,8 @@ export const useMessages = (initialParams?: ListMessagesParams) => {
const newMessages = newData?.messages || []
const totalPages = newData?.totalPages || 1
hasMoreRef.current = nextPage < totalPages
const hasMoreValue = nextPage < totalPages
setHasMore(hasMoreValue)
currentPageRef.current = nextPage
setData((prev) => ({
@@ -92,10 +94,10 @@ export const useMessages = (initialParams?: ListMessagesParams) => {
}))
setLoadingMore(false)
return { data: newData, error: null }
}, [loading, loadingMore, initialParams])
}, [loading, loadingMore, hasMore, initialParams])
const refetch = useCallback(async () => {
hasMoreRef.current = true
setHasMore(true)
setRefreshing(true)
const result = await execute()
setRefreshing(false)
@@ -108,11 +110,11 @@ export const useMessages = (initialParams?: ListMessagesParams) => {
loading,
loadingMore,
refreshing,
hasMore,
error,
execute,
refetch,
loadMore,
hasMore: hasMoreRef.current,
}
}

View File

@@ -24,8 +24,8 @@ export const useUserFavorites = (initialParams?: GetUserFavoritesParams) => {
const [loadingMore, setLoadingMore] = useState(false)
const [error, setError] = useState<ApiError | null>(null)
const [data, setData] = useState<GetUserFavoritesResponse>()
const [hasMore, setHasMore] = useState(true)
const currentPageRef = useRef(1)
const hasMoreRef = useRef(true)
const execute = useCallback(async (params?: GetUserFavoritesParams) => {
setLoading(true)
@@ -55,14 +55,15 @@ export const useUserFavorites = (initialParams?: GetUserFavoritesParams) => {
const total = data?.total || 0
const limit = requestParams.limit || 20
const totalPages = Math.ceil(total / limit)
hasMoreRef.current = currentPage < totalPages
const hasMoreValue = currentPage < totalPages
setHasMore(hasMoreValue)
setData(data)
setLoading(false)
return { data, error: null }
}, [initialParams])
const loadMore = useCallback(async () => {
if (loadingMore || loading || !hasMoreRef.current) return { data: undefined, error: null }
if (loadingMore || loading || !hasMore) return { data: undefined, error: null }
setLoadingMore(true)
const nextPage = currentPageRef.current + 1
@@ -87,7 +88,8 @@ export const useUserFavorites = (initialParams?: GetUserFavoritesParams) => {
const total = newData?.total || 0
const limit = requestParams.limit || 20
const totalPages = Math.ceil(total / limit)
hasMoreRef.current = nextPage < totalPages
const hasMoreValue = nextPage < totalPages
setHasMore(hasMoreValue)
currentPageRef.current = nextPage
setData((prev) => ({
@@ -98,10 +100,10 @@ export const useUserFavorites = (initialParams?: GetUserFavoritesParams) => {
}))
setLoadingMore(false)
return { data: newData, error: null }
}, [loading, loadingMore, initialParams])
}, [loading, loadingMore, hasMore, initialParams])
const refetch = useCallback(() => {
hasMoreRef.current = true
setHasMore(true)
return execute()
}, [execute])
@@ -110,11 +112,11 @@ export const useUserFavorites = (initialParams?: GetUserFavoritesParams) => {
favorites: data?.favorites || [],
loading,
loadingMore,
hasMore,
error,
execute,
refetch,
loadMore,
hasMore: hasMoreRef.current,
}
}

View File

@@ -21,8 +21,8 @@ export const useUserLikes = (initialParams?: GetUserLikesParams) => {
const [loadingMore, setLoadingMore] = useState(false)
const [error, setError] = useState<ApiError | null>(null)
const [data, setData] = useState<GetUserLikesResponse>()
const [hasMore, setHasMore] = useState(true)
const currentPageRef = useRef(1)
const hasMoreRef = useRef(true)
const execute = useCallback(async (params?: GetUserLikesParams) => {
setLoading(true)
@@ -52,14 +52,15 @@ export const useUserLikes = (initialParams?: GetUserLikesParams) => {
const total = data?.total || 0
const limit = requestParams.limit || 20
const totalPages = Math.ceil(total / limit)
hasMoreRef.current = currentPage < totalPages
const hasMoreValue = currentPage < totalPages
setHasMore(hasMoreValue)
setData(data)
setLoading(false)
return { data, error: null }
}, [initialParams])
const loadMore = useCallback(async () => {
if (loadingMore || loading || !hasMoreRef.current) return { data: undefined, error: null }
if (loadingMore || loading || !hasMore) return { data: undefined, error: null }
setLoadingMore(true)
const nextPage = currentPageRef.current + 1
@@ -84,7 +85,8 @@ export const useUserLikes = (initialParams?: GetUserLikesParams) => {
const total = newData?.total || 0
const limit = requestParams.limit || 20
const totalPages = Math.ceil(total / limit)
hasMoreRef.current = nextPage < totalPages
const hasMoreValue = nextPage < totalPages
setHasMore(hasMoreValue)
currentPageRef.current = nextPage
setData((prev) => ({
@@ -93,10 +95,10 @@ export const useUserLikes = (initialParams?: GetUserLikesParams) => {
}))
setLoadingMore(false)
return { data: newData, error: null }
}, [loading, loadingMore, initialParams])
}, [loading, loadingMore, hasMore, initialParams])
const refetch = useCallback(() => {
hasMoreRef.current = true
setHasMore(true)
return execute()
}, [execute])
@@ -105,10 +107,10 @@ export const useUserLikes = (initialParams?: GetUserLikesParams) => {
likes: data?.likes || [],
loading,
loadingMore,
hasMore,
error,
execute,
refetch,
loadMore,
hasMore: hasMoreRef.current,
}
}