Migrate frontend realtime messaging to SSE.
Switch service integrations and screen/store consumers from websocket events to SSE, and ignore generated dist-web artifacts. Made-with: Cursor
This commit is contained in:
@@ -20,14 +20,13 @@ import {
|
||||
Alert,
|
||||
ScrollView,
|
||||
Image,
|
||||
Clipboard,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
|
||||
import { Post, Comment, VoteResultDTO } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
@@ -35,11 +34,11 @@ import { useCurrentUser } from '../../stores/authStore';
|
||||
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
|
||||
import { CommentItem, VoteCard } from '../../components/business';
|
||||
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout } from '../../components/common';
|
||||
import { HomeStackParamList } from '../../navigation/types';
|
||||
import { RootStackParamList } from '../../navigation/types';
|
||||
import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'PostDetail'>;
|
||||
type PostDetailRouteProp = RouteProp<HomeStackParamList, 'PostDetail'>;
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'PostDetail'>;
|
||||
type PostDetailRouteProp = RouteProp<RootStackParamList, 'PostDetail'>;
|
||||
|
||||
export const PostDetailScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
@@ -181,6 +180,13 @@ export const PostDetailScreen: React.FC = () => {
|
||||
loadPostDetail(true);
|
||||
}, [loadPostDetail]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = navigation.addListener('focus', () => {
|
||||
loadPostDetail(false);
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [navigation, loadPostDetail]);
|
||||
|
||||
// 如果是从评论按钮跳转过来的,加载完成后滚动到评论区
|
||||
useEffect(() => {
|
||||
if (shouldScrollToComments && !loading && comments.length > 0) {
|
||||
@@ -262,16 +268,44 @@ export const PostDetailScreen: React.FC = () => {
|
||||
setRefreshing(false);
|
||||
}, [loadPostDetail]);
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string): string => {
|
||||
try {
|
||||
return formatDistanceToNow(new Date(dateString), {
|
||||
addSuffix: true,
|
||||
locale: zhCN,
|
||||
});
|
||||
} catch {
|
||||
return '';
|
||||
const formatDateTime = (dateString?: string | null): string => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
const pad = (num: number) => String(num).padStart(2, '0');
|
||||
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
const formatRelativeTime = (dateString?: string | null): string => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
|
||||
const now = Date.now();
|
||||
const diffMs = now - date.getTime();
|
||||
if (diffMs < 0) return formatDateTime(dateString);
|
||||
|
||||
const minuteMs = 60 * 1000;
|
||||
const hourMs = 60 * minuteMs;
|
||||
const dayMs = 24 * hourMs;
|
||||
|
||||
if (diffMs < minuteMs) return '刚刚';
|
||||
if (diffMs < hourMs) return `${Math.floor(diffMs / minuteMs)}分钟前`;
|
||||
if (diffMs < dayMs) return `${Math.floor(diffMs / hourMs)}小时前`;
|
||||
if (diffMs < 2 * dayMs) {
|
||||
const pad = (num: number) => String(num).padStart(2, '0');
|
||||
return `昨天 ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
return formatDateTime(dateString);
|
||||
};
|
||||
|
||||
const isPostEdited = (createdAt?: string, updatedAt?: string): boolean => {
|
||||
if (!createdAt || !updatedAt) return false;
|
||||
const created = new Date(createdAt).getTime();
|
||||
const updated = new Date(updatedAt).getTime();
|
||||
if (Number.isNaN(created) || Number.isNaN(updated)) return false;
|
||||
return updated-created > 1000;
|
||||
};
|
||||
|
||||
// 格式化数字
|
||||
@@ -350,9 +384,16 @@ export const PostDetailScreen: React.FC = () => {
|
||||
}, [post, favoritePost, unfavoritePost]);
|
||||
|
||||
// 分享帖子
|
||||
const handleShare = useCallback(() => {
|
||||
// TODO: 实现分享功能
|
||||
void post;
|
||||
const handleShare = useCallback(async () => {
|
||||
if (!post?.id) return;
|
||||
try {
|
||||
await postService.sharePost(post.id);
|
||||
} catch (error) {
|
||||
console.error('上报分享次数失败:', error);
|
||||
}
|
||||
const postUrl = `https://browser.littlelan.cn/posts/${encodeURIComponent(post.id)}`;
|
||||
Clipboard.setString(postUrl);
|
||||
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
||||
}, [post?.id]);
|
||||
|
||||
// 投票处理函数
|
||||
@@ -484,6 +525,14 @@ export const PostDetailScreen: React.FC = () => {
|
||||
);
|
||||
}, [post, isDeleting, currentUser?.id, navigation]);
|
||||
|
||||
const handleEditPost = useCallback(() => {
|
||||
if (!post) return;
|
||||
navigation.navigate('CreatePost', {
|
||||
mode: 'edit',
|
||||
postId: post.id,
|
||||
});
|
||||
}, [navigation, post]);
|
||||
|
||||
// 点击图片查看大图
|
||||
const handleImagePress = useCallback((images: ImageGridItem[], index: number) => {
|
||||
setAllImages(images);
|
||||
@@ -1012,33 +1061,59 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
{/* 发帖时间和浏览量 - 放在图片下方 */}
|
||||
<View style={[styles.postMetaInfo, { marginTop: responsiveGap }]}>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||
{formatTime(post.created_at)}
|
||||
</Text>
|
||||
{post.views_count !== undefined && post.views_count > 0 && (
|
||||
<>
|
||||
<Text style={styles.metaInfoDot}>·</Text>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||
{formatNumber(post.views_count)} 浏览
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{/* 删除按钮 - 只对帖子作者显示 */}
|
||||
<View style={styles.metaInfoMain}>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||
发布 {formatRelativeTime(post.created_at)}
|
||||
</Text>
|
||||
{isPostEdited(post.created_at, post.updated_at) && (
|
||||
<>
|
||||
<Text style={styles.metaInfoDot}>·</Text>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||
修改 {formatRelativeTime(post.updated_at)}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{post.views_count !== undefined && post.views_count > 0 && (
|
||||
<>
|
||||
<Text style={styles.metaInfoDot}>·</Text>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.metaInfoText}>
|
||||
{formatNumber(post.views_count)} 浏览
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{currentUser?.id === post.author?.id && (
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButtonInline}
|
||||
onPress={handleDeletePost}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={isDeleting ? 'loading' : 'delete-outline'}
|
||||
size={14}
|
||||
color={colors.text.hint}
|
||||
/>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.deleteButtonText}>
|
||||
删除
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.metaActions}>
|
||||
<TouchableOpacity
|
||||
style={styles.editButtonInline}
|
||||
onPress={handleEditPost}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="pencil-outline"
|
||||
size={14}
|
||||
color={colors.text.hint}
|
||||
/>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.editButtonText}>
|
||||
编辑
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{/* 删除按钮 - 只对帖子作者显示 */}
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButtonInline}
|
||||
onPress={handleDeletePost}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={isDeleting ? 'loading' : 'delete-outline'}
|
||||
size={14}
|
||||
color={colors.text.hint}
|
||||
/>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.deleteButtonText}>
|
||||
删除
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1107,7 +1182,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}, [post, postImages, currentUser?.id, isDeleting, handleLike, handleShare, handleFavorite, handleDeletePost, handleImagePress, voteResult, isVoteLoading, handleVote, handleUnvote, isDesktop, isTablet, isWideScreen, responsivePadding, responsiveGap]);
|
||||
}, [post, postImages, currentUser?.id, isDeleting, handleLike, handleShare, handleFavorite, handleDeletePost, handleEditPost, handleImagePress, voteResult, isVoteLoading, handleVote, handleUnvote, isDesktop, isTablet, isWideScreen, responsivePadding, responsiveGap]);
|
||||
|
||||
// 回复评论
|
||||
const [replyingTo, setReplyingTo] = useState<Comment | null>(null);
|
||||
@@ -1548,8 +1623,16 @@ const styles = StyleSheet.create({
|
||||
postMetaInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
metaInfoMain: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
metaInfoText: {
|
||||
fontSize: fontSizes.sm,
|
||||
color: colors.text.hint,
|
||||
@@ -1563,13 +1646,28 @@ const styles = StyleSheet.create({
|
||||
deleteButtonInline: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 'auto',
|
||||
marginLeft: spacing.sm,
|
||||
padding: spacing.xs,
|
||||
},
|
||||
deleteButtonText: {
|
||||
marginLeft: 2,
|
||||
fontSize: fontSizes.sm,
|
||||
},
|
||||
metaActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: spacing.sm,
|
||||
flexShrink: 0,
|
||||
},
|
||||
editButtonInline: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: spacing.xs,
|
||||
},
|
||||
editButtonText: {
|
||||
marginLeft: 2,
|
||||
fontSize: fontSizes.sm,
|
||||
},
|
||||
imagesContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
|
||||
Reference in New Issue
Block a user