Files
bw-expo-app/components/profile/profile-screen.tsx
imeepos eee280d9b3 🐛 修复视频播放器无限循环渲染问题
## 主要修复
- 修复 FullscreenMediaModal 和 FullscreenVideoModal 中的 useEffect 循环依赖
- 重构 VideoPlayer 组件的视频属性管理逻辑
- 优化 useVideoPlayer 的初始化回调机制

## 新增功能
- 新增标签 API 支持 (lib/api/tags.ts)
- 新增内容骨架屏组件 (components/profile/content-skeleton.tsx)
- 新增返回按钮组件 (components/ui/back-button.tsx)

## 改进优化
- 优化视频播放器的性能,避免重复初始化
- 修复 useEffect 依赖项导致的无限循环更新
- 完善类型定义和 API 接口

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 11:58:56 +08:00

155 lines
4.7 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { View, useWindowDimensions, type ImageSourcePropType } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useAuth } from '@/hooks/use-auth';
import { useProfileData } from '@/hooks/use-profile-data';
import { createStats, deriveAvatarSource, deriveDisplayName, deriveNumericValue } from '@/utils/profile-data';
import { StyleSheet } from 'react-native';
import { ContentGallery } from './content-gallery';
import { ContentTabs } from './content-tabs';
import { Divider } from './divider';
import { ProfileEditModal } from './profile-edit-modal';
import { ProfileEmptyState } from './profile-empty-state';
import { ProfileErrorState } from './profile-error-state';
import { ProfileHeader } from './profile-header';
import { ProfileIdentity } from './profile-identity';
import { ProfileLoadingState } from './profile-loading-state';
import { ContentSkeleton } from './content-skeleton';
type TabKey = 'all' | 'image' | 'video';
type BillingMode = 'monthly' | 'lifetime';
export function ProfileScreen() {
const { user } = useAuth();
const { width } = useWindowDimensions();
const insets = useSafeAreaInsets();
const [billingMode, setBillingMode] = useState<BillingMode>('monthly');
const [activeTab, setActiveTab] = useState<TabKey>('all');
const displayNameFromUser = deriveDisplayName(user) ?? 'prairie_pufferfish_the';
const [presentedDisplayName, setPresentedDisplayName] = useState(displayNameFromUser);
const [isEditingIdentity, setIsEditingIdentity] = useState(false);
const {
generations,
isLoading,
error,
isRefreshing,
isLoadingMore,
hasMore,
handleRefresh,
handleLoadMore,
} = useProfileData(activeTab);
const horizontalPadding = width < 400 ? 20 : 24;
const avatarSize = width < 400 ? 74 : 82;
const avatarSource = deriveAvatarSource(user);
const creditBalance = deriveNumericValue((user as Record<string, unknown>)?.credits);
const stats = createStats(user);
const [presentedAvatar, setPresentedAvatar] = useState<ImageSourcePropType | undefined>(avatarSource);
useEffect(() => {
setPresentedDisplayName(displayNameFromUser);
}, [displayNameFromUser]);
useEffect(() => {
setPresentedAvatar(avatarSource);
}, [avatarSource]);
if (isLoading) {
return (
<View style={[styles.screen, { backgroundColor: stylesVars.background }]}>
{insets.top > 0 && <View style={{ height: insets.top }} />}
<ProfileLoadingState />
</View>
);
}
if (error) {
return (
<View style={[styles.screen, { backgroundColor: stylesVars.background }]}>
{insets.top > 0 && <View style={{ height: insets.top }} />}
<ProfileErrorState onRetry={handleRefresh} />
</View>
);
}
return (
<View style={[styles.screen, { backgroundColor: stylesVars.background }]}>
{insets.top > 0 && <View style={{ height: insets.top }} />}
<View style={{ paddingHorizontal: horizontalPadding }}>
<ProfileHeader
billingMode={billingMode}
onChangeBilling={setBillingMode}
credits={creditBalance}
/>
<ProfileIdentity
displayName={presentedDisplayName}
avatarSource={presentedAvatar}
avatarSize={avatarSize}
stats={stats}
onEdit={() => setIsEditingIdentity(true)}
/>
<Divider />
<ContentTabs
activeTab={activeTab}
onChangeTab={setActiveTab}
isLoading={isLoading && generations.length > 0}
/>
<Divider />
</View>
{isLoading && generations.length === 0 ? (
<ContentSkeleton />
) : generations.length === 0 ? (
<ProfileEmptyState activeTab={activeTab} />
) : (
<View style={[styles.galleryContainer, { paddingHorizontal: horizontalPadding / 2 }]}>
<ContentGallery
generations={generations}
isRefreshing={isRefreshing}
onRefresh={handleRefresh}
isLoadingMore={isLoadingMore}
hasMore={hasMore}
onLoadMore={handleLoadMore}
/>
</View>
)}
<ProfileEditModal
visible={isEditingIdentity}
avatarSource={presentedAvatar}
initialName={presentedDisplayName}
onClose={() => setIsEditingIdentity(false)}
onSave={({ name, avatar }) => {
setPresentedDisplayName(name);
if (avatar) {
setPresentedAvatar(avatar);
}
setIsEditingIdentity(false);
}}
/>
</View>
);
}
const stylesVars = {
background: '#050505',
paddingBottom: 96,
};
const styles = StyleSheet.create({
screen: {
flex: 1,
},
galleryContainer: {
paddingTop: 8,
},
});
export default ProfileScreen;