refactor(platform): extract blurActiveElement to infrastructure module
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 4m47s
Frontend CI / ota-android (push) Successful in 10m48s
Frontend CI / build-android-apk (push) Successful in 1h36m37s

- Centralize duplicated Platform.OS === 'web' blur logic across 16 components
- Add blurActiveElement utility to src/infrastructure/platform module
- Optimize MessageBubble and ImageSegment with React.memo custom comparators
- Add useMemo for computed values in MessageSegmentsRenderer
- Update DTO types with is_system_notice and notice_content fields
- Fix keyboard dismissal order in useChatScreen handleDismiss
- Simplify userStore follow/unfollow state updates
- Remove unnecessary TypeScript type assertions throughout codebase
This commit is contained in:
lafay
2026-04-11 22:35:11 +08:00
parent 6f84e17772
commit 4b5ce1ba21
29 changed files with 679 additions and 222 deletions

View File

@@ -38,6 +38,7 @@ import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
import { SearchScreen } from './SearchScreen';
import { CreatePostScreen } from '../create/CreatePostScreen';
import * as hrefs from '../../navigation/hrefs';
import { blurActiveElement } from '../../infrastructure/platform';
const TABS = ['关注', '最新', '热门'];
const TAB_ICONS = ['account-heart-outline', 'clock-outline', 'fire'];
@@ -203,9 +204,8 @@ export const HomeScreen: React.FC = () => {
const [showCreatePost, setShowCreatePost] = useState(false);
useEffect(() => {
if (showCreatePost && Platform.OS === 'web') {
const active = (globalThis as any)?.document?.activeElement as { blur?: () => void } | undefined;
active?.blur?.();
if (showCreatePost) {
blurActiveElement();
}
}, [showCreatePost]);

View File

@@ -42,8 +42,35 @@ import { useCursorPagination } from '../../hooks/useCursorPagination';
import { CommentItem, VoteCard, ReportDialog } from '../../components/business';
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout, AppBackButton } from '../../components/common';
import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../hooks/useResponsive';
import { handleError } from '../../services/errorHandler';
import * as hrefs from '../../navigation/hrefs';
function togglePostField(
post: Post,
field: 'is_liked' | 'is_favorited',
countField: 'likes_count' | 'favorites_count',
): Post {
const oldValue = post[field];
const oldCount = post[countField];
return {
...post,
[field]: !oldValue,
[countField]: oldValue ? Math.max(0, oldCount - 1) : oldCount + 1,
};
}
function toggleCommentLikeInTree(comments: Comment[], commentId: string, isLiked: boolean, likesCount: number): Comment[] {
return comments.map(c => {
if (c.id === commentId) {
return { ...c, is_liked: isLiked, likes_count: likesCount };
}
if (c.replies?.length) {
return { ...c, replies: toggleCommentLikeInTree(c.replies, commentId, isLiked, likesCount) };
}
return c;
});
}
export const PostDetailScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createPostDetailStyles(colors), [colors]);
@@ -217,20 +244,17 @@ export const PostDetailScreen: React.FC = () => {
setPost(postData as unknown as Post);
// 初始化关注状态
if (postData.author) {
setIsFollowing((postData.author as any).is_following || false);
setIsFollowingMe((postData.author as any).is_following_me || false);
setIsFollowing(postData.author.is_following || false);
setIsFollowingMe(postData.author.is_following_me || false);
}
// 只在首次加载时记录浏览量
if (recordView && !hasRecordedView.current) {
if (!hasRecordedView.current) {
hasRecordedView.current = true;
// 异步记录浏览量,不阻塞加载
postService.recordView(postId).catch(err => {
console.error('记录浏览量失败:', err);
});
}
// 如果是投票帖子,立即加载投票数据
if ((postData as any).is_vote) {
if (postData.is_vote) {
setIsVoteLoading(true);
try {
const voteData = await voteService.getVoteResult(postId);
@@ -251,8 +275,8 @@ export const PostDetailScreen: React.FC = () => {
setPost(updatedPost);
// 初始化关注状态
if (updatedPost.author) {
setIsFollowing((updatedPost.author as any).is_following || false);
setIsFollowingMe((updatedPost.author as any).is_following_me || false);
setIsFollowing(updatedPost.author.is_following || false);
setIsFollowingMe(updatedPost.author.is_following_me || false);
}
}
}
@@ -260,7 +284,7 @@ export const PostDetailScreen: React.FC = () => {
// 加载评论(使用游标分页刷新)
await refreshComments();
} catch (error) {
console.error('加载帖子详情失败:', error);
handleError(error, { context: '加载帖子详情' });
} finally {
setIsPostInitialLoading(false);
}
@@ -458,65 +482,36 @@ export const PostDetailScreen: React.FC = () => {
const handleLike = useCallback(async () => {
if (!post) return;
// 先保存旧状态用于回滚
const oldIsLiked = post.is_liked;
const oldLikesCount = post.likes_count;
// 乐观更新本地状态
setPost(prev => prev ? {
...prev,
is_liked: !prev.is_liked,
likes_count: prev.is_liked ? prev.likes_count - 1 : prev.likes_count + 1
} : null);
const originalPost = post;
setPost(prev => prev ? togglePostField(prev, 'is_liked', 'likes_count') : null);
try {
if (oldIsLiked) {
if (originalPost.is_liked) {
await processPostUseCase.unlikePost(post.id);
} else {
await processPostUseCase.likePost(post.id);
}
// UseCase 会更新 store本地状态已经是乐观更新的无需再次更新
} catch (error) {
console.error('点赞操作失败:', error);
// 失败时回滚状态
setPost(prev => prev ? {
...prev,
is_liked: oldIsLiked,
likes_count: oldLikesCount
} : null);
handleError(error, { context: '点赞' });
setPost(originalPost);
}
}, [post]);
// 收藏帖子
const handleFavorite = useCallback(async () => {
if (!post) return;
// 先保存旧状态用于回滚
const oldIsFavorited = post.is_favorited;
const oldFavoritesCount = post.favorites_count;
// 乐观更新本地状态
setPost(prev => prev ? {
...prev,
is_favorited: !prev.is_favorited,
favorites_count: prev.is_favorited ? prev.favorites_count - 1 : prev.favorites_count + 1
} : null);
const originalPost = post;
setPost(prev => prev ? togglePostField(prev, 'is_favorited', 'favorites_count') : null);
try {
if (oldIsFavorited) {
if (originalPost.is_favorited) {
await processPostUseCase.unfavoritePost(post.id);
} else {
await processPostUseCase.favoritePost(post.id);
}
// UseCase 会更新 store本地状态已经是乐观更新的无需再次更新
} catch (error) {
console.error('收藏操作失败:', error);
// 失败时回滚状态
setPost(prev => prev ? {
...prev,
is_favorited: oldIsFavorited,
favorites_count: oldFavoritesCount
} : null);
handleError(error, { context: '收藏' });
setPost(originalPost);
}
}, [post]);
@@ -573,24 +568,19 @@ export const PostDetailScreen: React.FC = () => {
setVoteResult(oldVoteResult);
}
} catch (error) {
console.error('投票失败:', error);
// 失败时回滚
handleError(error, { context: '投票' });
setVoteResult(oldVoteResult);
} finally {
setIsVoteLoading(false);
}
}, [post, voteResult, isVoteLoading]);
// 取消投票处理函数
const handleUnvote = useCallback(async () => {
if (!post || !voteResult || isVoteLoading || !voteResult.voted_option_id) return;
const votedOptionId = voteResult.voted_option_id;
// 保存旧状态用于回滚
const oldVoteResult = { ...voteResult };
// 乐观更新
setVoteResult((prev: VoteResultDTO | null) => {
if (!prev) return null;
return {
@@ -611,12 +601,10 @@ export const PostDetailScreen: React.FC = () => {
try {
const success = await voteService.unvote(post.id);
if (!success) {
// 失败时回滚
setVoteResult(oldVoteResult);
}
} catch (error) {
console.error('取消投票失败:', error);
// 失败时回滚
handleError(error, { context: '取消投票' });
setVoteResult(oldVoteResult);
} finally {
setIsVoteLoading(false);
@@ -899,26 +887,15 @@ export const PostDetailScreen: React.FC = () => {
}
};
// 点赞评论(包括回复)- 优化版
const handleLikeComment = async (comment: Comment) => {
const { id, is_liked, likes_count } = comment;
// 乐观更新:切换点赞状态
const newIsLiked = !is_liked;
const oldIsLiked = is_liked ?? false;
const newIsLiked = !oldIsLiked;
const newLikesCount = newIsLiked ? likes_count + 1 : Math.max(0, likes_count - 1);
const originalComments = comments;
// 递归更新评论树中的点赞状态
const updateLikeInTree = (c: Comment): Comment => {
if (c.id === id) {
return { ...c, is_liked: newIsLiked, likes_count: newLikesCount };
}
if (c.replies?.length) {
return { ...c, replies: c.replies.map(r => updateLikeInTree(r)) };
}
return c;
};
setComments(prev => prev.map(c => updateLikeInTree(c)));
setComments(prev => toggleCommentLikeInTree(prev, id, newIsLiked, newLikesCount));
try {
const success = newIsLiked
@@ -929,18 +906,8 @@ export const PostDetailScreen: React.FC = () => {
throw new Error('点赞操作失败');
}
} catch (error) {
console.error('评论点赞操作失败:', error);
// 回滚状态
const rollbackLikeInTree = (c: Comment): Comment => {
if (c.id === id) {
return { ...c, is_liked, likes_count };
}
if (c.replies?.length) {
return { ...c, replies: c.replies.map(r => rollbackLikeInTree(r)) };
}
return c;
};
setComments(prev => prev.map(c => rollbackLikeInTree(c)));
handleError(error, { context: '评论点赞' });
setComments(toggleCommentLikeInTree(originalComments, id, oldIsLiked, likes_count));
}
};