dev #5
@@ -86,7 +86,9 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const isClosingRef = useRef(false);
|
const isClosingRef = useRef(false);
|
||||||
|
const currentIndexRef = useRef(initialIndex);
|
||||||
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
||||||
|
currentIndexRef.current = currentIndex;
|
||||||
const [showControls, setShowControls] = useState(true);
|
const [showControls, setShowControls] = useState(true);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
@@ -152,16 +154,19 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 图片变化时重置加载状态和缩放
|
// 图片变化时重置缩放(加载态在 updateIndex / 打开弹窗时同步设置,避免晚一帧仍显示上一张)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
|
||||||
setError(false);
|
|
||||||
resetZoom();
|
resetZoom();
|
||||||
}, [currentImage?.id, resetZoom]);
|
}, [currentImage?.id, resetZoom]);
|
||||||
|
|
||||||
const updateIndex = useCallback(
|
const updateIndex = useCallback(
|
||||||
(newIndex: number) => {
|
(newIndex: number) => {
|
||||||
const clampedIndex = Math.max(0, Math.min(validImages.length - 1, newIndex));
|
const clampedIndex = Math.max(0, Math.min(validImages.length - 1, newIndex));
|
||||||
|
if (clampedIndex === currentIndexRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
setCurrentIndex(clampedIndex);
|
setCurrentIndex(clampedIndex);
|
||||||
onIndexChange?.(clampedIndex);
|
onIndexChange?.(clampedIndex);
|
||||||
},
|
},
|
||||||
@@ -412,7 +417,8 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|||||||
contentFit="contain"
|
contentFit="contain"
|
||||||
cachePolicy="disk"
|
cachePolicy="disk"
|
||||||
priority="high"
|
priority="high"
|
||||||
recyclingKey={currentImage.id}
|
recyclingKey={currentImage.url}
|
||||||
|
transition={null}
|
||||||
allowDownscaling
|
allowDownscaling
|
||||||
onLoadStart={() => setLoading(true)}
|
onLoadStart={() => setLoading(true)}
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
@@ -556,11 +562,13 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
zIndex: 5,
|
zIndex: 5,
|
||||||
|
backgroundColor: '#000',
|
||||||
},
|
},
|
||||||
errorContainer: {
|
errorContainer: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#000',
|
||||||
},
|
},
|
||||||
errorText: {
|
errorText: {
|
||||||
color: '#999',
|
color: '#999',
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export interface GetPostsParams {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
/** 游标 - 用于无限滚动加载 */
|
/** 游标 - 用于无限滚动加载 */
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
/** 帖子类型筛选(可选):recommend, follow, hot, latest */
|
/** 帖子类型筛选(可选):follow, hot, latest */
|
||||||
post_type?: 'recommend' | 'follow' | 'hot' | 'latest';
|
post_type?: 'follow' | 'hot' | 'latest';
|
||||||
/** 社区ID过滤 */
|
/** 社区ID过滤 */
|
||||||
communityId?: string;
|
communityId?: string;
|
||||||
/** 作者ID过滤 */
|
/** 作者ID过滤 */
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ const prefetchService = new PrefetchService();
|
|||||||
* 预取帖子数据
|
* 预取帖子数据
|
||||||
* @param types 帖子类型数组
|
* @param types 帖子类型数组
|
||||||
*/
|
*/
|
||||||
function prefetchPosts(types: string[] = ['recommend', 'hot']): void {
|
function prefetchPosts(types: string[] = ['latest', 'hot']): void {
|
||||||
types.forEach((type, index) => {
|
types.forEach((type, index) => {
|
||||||
prefetchService.schedule({
|
prefetchService.schedule({
|
||||||
key: `posts:${type}:1`,
|
key: `posts:${type}:1`,
|
||||||
@@ -230,7 +230,7 @@ function prefetchOnAppLaunch(): void {
|
|||||||
prefetchConversations();
|
prefetchConversations();
|
||||||
|
|
||||||
// 中优先级:帖子列表
|
// 中优先级:帖子列表
|
||||||
prefetchPosts(['recommend']);
|
prefetchPosts(['latest']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -257,7 +257,7 @@ function prefetchMessageScreen(): void {
|
|||||||
* 进入首页时预取
|
* 进入首页时预取
|
||||||
*/
|
*/
|
||||||
function prefetchHomeScreen(): void {
|
function prefetchHomeScreen(): void {
|
||||||
prefetchPosts(['recommend', 'hot', 'latest']);
|
prefetchPosts(['latest', 'hot', 'follow']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== React Hook ====================
|
// ==================== React Hook ====================
|
||||||
|
|||||||
@@ -178,6 +178,12 @@ export function SimpleMobileTabNavigator() {
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const messageUnreadCount = useTotalUnreadCount();
|
const messageUnreadCount = useTotalUnreadCount();
|
||||||
const [activeTab, setActiveTab] = useState<TabName>('HomeTab');
|
const [activeTab, setActiveTab] = useState<TabName>('HomeTab');
|
||||||
|
const [mountedTabs, setMountedTabs] = useState<Record<TabName, boolean>>({
|
||||||
|
HomeTab: true,
|
||||||
|
MessageTab: false,
|
||||||
|
ScheduleTab: false,
|
||||||
|
ProfileTab: false,
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化 MessageManager
|
// 初始化 MessageManager
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -186,12 +192,21 @@ export function SimpleMobileTabNavigator() {
|
|||||||
|
|
||||||
// 处理 Tab 切换
|
// 处理 Tab 切换
|
||||||
const handleTabPress = useCallback((tabName: TabName) => {
|
const handleTabPress = useCallback((tabName: TabName) => {
|
||||||
|
setMountedTabs((prev) => {
|
||||||
|
if (prev[tabName]) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[tabName]: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
setActiveTab(tabName);
|
setActiveTab(tabName);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 渲染当前 Tab 的内容
|
// 根据 Tab 名称渲染对应内容
|
||||||
const renderTabContent = () => {
|
const renderTabView = (tabName: TabName) => {
|
||||||
switch (activeTab) {
|
switch (tabName) {
|
||||||
case 'HomeTab':
|
case 'HomeTab':
|
||||||
return <HomeScreen />;
|
return <HomeScreen />;
|
||||||
case 'MessageTab':
|
case 'MessageTab':
|
||||||
@@ -276,7 +291,24 @@ export function SimpleMobileTabNavigator() {
|
|||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{renderTabContent()}
|
{(Object.keys(mountedTabs) as TabName[]).map((tabName) => {
|
||||||
|
if (!mountedTabs[tabName]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isActive = activeTab === tabName;
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={tabName}
|
||||||
|
style={[
|
||||||
|
styles.tabScreen,
|
||||||
|
!isActive && styles.tabScreenHidden,
|
||||||
|
]}
|
||||||
|
pointerEvents={isActive ? 'auto' : 'none'}
|
||||||
|
>
|
||||||
|
{renderTabView(tabName)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Tab Bar */}
|
{/* Tab Bar */}
|
||||||
@@ -334,6 +366,13 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
tabScreen: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
},
|
||||||
|
tabScreenHidden: {
|
||||||
|
display: 'none',
|
||||||
},
|
},
|
||||||
bottomSpacer: {
|
bottomSpacer: {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ import { navigationService } from '../../infrastructure/navigation/navigationSer
|
|||||||
|
|
||||||
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'Home'> & NativeStackNavigationProp<RootStackParamList>;
|
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'Home'> & NativeStackNavigationProp<RootStackParamList>;
|
||||||
|
|
||||||
const TABS = ['推荐', '关注', '热门', '最新'];
|
const TABS = ['最新', '关注', '热门'];
|
||||||
const TAB_ICONS = ['compass-outline', 'account-heart-outline', 'fire', 'clock-outline'];
|
const TAB_ICONS = ['clock-outline', 'account-heart-outline', 'fire'];
|
||||||
const DEFAULT_PAGE_SIZE = 20;
|
const DEFAULT_PAGE_SIZE = 20;
|
||||||
const SWIPE_TRANSLATION_THRESHOLD = 40;
|
const SWIPE_TRANSLATION_THRESHOLD = 40;
|
||||||
const SWIPE_COOLDOWN_MS = 300;
|
const SWIPE_COOLDOWN_MS = 300;
|
||||||
@@ -51,7 +51,7 @@ const MOBILE_TAB_FLOATING_MARGIN = 12;
|
|||||||
const MOBILE_FAB_GAP = 12;
|
const MOBILE_FAB_GAP = 12;
|
||||||
|
|
||||||
type ViewMode = 'list' | 'grid';
|
type ViewMode = 'list' | 'grid';
|
||||||
type PostType = 'recommend' | 'follow' | 'hot' | 'latest';
|
type PostType = 'follow' | 'hot' | 'latest';
|
||||||
|
|
||||||
export const HomeScreen: React.FC = () => {
|
export const HomeScreen: React.FC = () => {
|
||||||
const navigation = useNavigation<NavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
@@ -106,11 +106,10 @@ export const HomeScreen: React.FC = () => {
|
|||||||
// 获取当前 tab 对应的帖子类型
|
// 获取当前 tab 对应的帖子类型
|
||||||
const getPostType = useCallback((): PostType => {
|
const getPostType = useCallback((): PostType => {
|
||||||
switch (activeIndex) {
|
switch (activeIndex) {
|
||||||
case 0: return 'recommend';
|
case 0: return 'latest';
|
||||||
case 1: return 'follow';
|
case 1: return 'follow';
|
||||||
case 2: return 'hot';
|
case 2: return 'hot';
|
||||||
case 3: return 'latest';
|
default: return 'latest';
|
||||||
default: return 'recommend';
|
|
||||||
}
|
}
|
||||||
}, [activeIndex]);
|
}, [activeIndex]);
|
||||||
|
|
||||||
|
|||||||
@@ -63,11 +63,6 @@ class PostService {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取推荐帖子
|
|
||||||
async getRecommendedPosts(page = 1, pageSize = 20): Promise<PaginatedData<Post>> {
|
|
||||||
return this.getPosts(page, pageSize, 'recommend');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取关注帖子
|
// 获取关注帖子
|
||||||
async getFollowingPosts(page = 1, pageSize = 20): Promise<PaginatedData<Post>> {
|
async getFollowingPosts(page = 1, pageSize = 20): Promise<PaginatedData<Post>> {
|
||||||
return this.getPosts(page, pageSize, 'follow');
|
return this.getPosts(page, pageSize, 'follow');
|
||||||
@@ -282,7 +277,7 @@ class PostService {
|
|||||||
/**
|
/**
|
||||||
* 获取帖子列表(游标分页)
|
* 获取帖子列表(游标分页)
|
||||||
* GET /api/v1/posts
|
* GET /api/v1/posts
|
||||||
* @param params 游标分页请求参数(包含 post_type 可选:recommend, follow, hot, latest)
|
* @param params 游标分页请求参数(包含 post_type 可选:follow, hot, latest)
|
||||||
*/
|
*/
|
||||||
async getPostsCursor(
|
async getPostsCursor(
|
||||||
params: CursorPaginationRequest = {}
|
params: CursorPaginationRequest = {}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPosts(
|
async getPosts(
|
||||||
type = 'recommend',
|
type = 'hot',
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 20,
|
pageSize = 20,
|
||||||
forceRefresh = false
|
forceRefresh = false
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ interface UserState {
|
|||||||
// 操作
|
// 操作
|
||||||
fetchUser: (userId: string) => Promise<User | undefined>;
|
fetchUser: (userId: string) => Promise<User | undefined>;
|
||||||
fetchUserPosts: (userId: string, page?: number) => Promise<Post[]>;
|
fetchUserPosts: (userId: string, page?: number) => Promise<Post[]>;
|
||||||
fetchPosts: (type: 'recommend' | 'follow' | 'hot' | 'latest', page?: number) => Promise<PaginatedData<Post>>;
|
fetchPosts: (type: 'follow' | 'hot' | 'latest', page?: number) => Promise<PaginatedData<Post>>;
|
||||||
fetchNotifications: (type?: string, page?: number) => Promise<Notification[]>;
|
fetchNotifications: (type?: string, page?: number) => Promise<Notification[]>;
|
||||||
markNotificationAsRead: (notificationId: string) => Promise<void>;
|
markNotificationAsRead: (notificationId: string) => Promise<void>;
|
||||||
markAllNotificationsAsRead: () => Promise<void>;
|
markAllNotificationsAsRead: () => Promise<void>;
|
||||||
@@ -108,16 +108,13 @@ export const useUserStore = create<UserState>((set, get) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 获取帖子列表(首页)
|
// 获取帖子列表(首页)
|
||||||
fetchPosts: async (type: 'recommend' | 'follow' | 'hot' | 'latest', page = 1) => {
|
fetchPosts: async (type: 'follow' | 'hot' | 'latest', page = 1) => {
|
||||||
set({ isLoadingPosts: true });
|
set({ isLoadingPosts: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'recommend':
|
|
||||||
response = await postService.getRecommendedPosts(page);
|
|
||||||
break;
|
|
||||||
case 'follow':
|
case 'follow':
|
||||||
response = await postService.getFollowingPosts(page);
|
response = await postService.getFollowingPosts(page);
|
||||||
break;
|
break;
|
||||||
@@ -332,7 +329,7 @@ export const useUserStore = create<UserState>((set, get) => {
|
|||||||
// 刷新所有数据
|
// 刷新所有数据
|
||||||
refreshAll: async () => {
|
refreshAll: async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
get().fetchPosts('recommend', 1),
|
get().fetchPosts('latest', 1),
|
||||||
get().fetchNotificationBadge(),
|
get().fetchNotificationBadge(),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -482,8 +482,8 @@ export interface CursorPaginationRequest {
|
|||||||
direction?: 'forward' | 'backward';
|
direction?: 'forward' | 'backward';
|
||||||
/** 每页数量(默认 20,最大 100) */
|
/** 每页数量(默认 20,最大 100) */
|
||||||
page_size?: number;
|
page_size?: number;
|
||||||
/** 帖子类型筛选(可选):recommend, follow, hot, latest */
|
/** 帖子类型筛选(可选):follow, hot, latest */
|
||||||
post_type?: 'recommend' | 'follow' | 'hot' | 'latest';
|
post_type?: 'follow' | 'hot' | 'latest';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user