feat(content): add rich content rendering with @mentions and vote segments
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 8m30s
Frontend CI / ota-android (push) Successful in 10m43s
Frontend CI / build-android-apk (push) Successful in 1h24m38s

- Add PostContentRenderer component for rendering posts/comments with segments
- Add PostMentionInput component for @mention support in post/comment creation
- Add VoteSegmentData and vote segment type for unified post creation API
- Update post and comment services to support segments in API requests
- Fix auth service to clear tokens before login/register
- Improve database initialization with proper close/retry logic
- Add verification check before allowing post creation
- Add metro web shims for react-native-webrtc full package and requireNativeComponent
- Update build config with git hash based version numbers
This commit is contained in:
lafay
2026-04-23 22:29:58 +08:00
parent eb4e1c080d
commit b2c3d5e54e
23 changed files with 797 additions and 127 deletions

View File

@@ -1,4 +1,4 @@
/**
/**
* 发帖页 CreatePostScreen响应式适配
* 威友 - 发布新帖子
* 参考微博发帖界面设计
@@ -38,8 +38,10 @@ import { channelService, postService, showPrompt, voteService } from '../../serv
import { ApiError } from '@/services/core';
import { uploadService } from '@/services/upload';
import VoteEditor from '../../components/business/VoteEditor';
import PostMentionInput from '../../components/business/PostMentionInput';
import { useResponsive, useResponsiveValue } from '../../hooks';
import * as hrefs from '../../navigation/hrefs';
import { MessageSegment } from '../../types';
// Props 接口
interface CreatePostScreenProps {
@@ -106,6 +108,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [segments, setSegments] = useState<MessageSegment[]>([]);
const [images, setImages] = useState<{ uri: string; uploading?: boolean }[]>([]);
const [channelOptions, setChannelOptions] = useState<ChannelOption[]>([]);
const [selectedChannelId, setSelectedChannelId] = useState<string | null>(null);
@@ -402,6 +405,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
const updated = await postService.updatePost(editPostID, {
title: title.trim(),
content: content.trim(),
segments: segments.length > 0 ? segments : undefined,
images: imageUrls,
});
if (!updated) {
@@ -415,10 +419,10 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
});
router.replace(hrefs.hrefHome());
} else {
// 创建普通帖子
await postService.createPost({
title: title.trim(),
content: content.trim(),
segments: segments.length > 0 ? segments : undefined,
images: imageUrls,
channel_id: selectedChannelId || undefined,
});
@@ -598,22 +602,18 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
maxLength={MAX_TITLE_LENGTH}
/>
{/* 正文输入 */}
<TextInput
ref={contentInputRef}
{/* 正文输入(支持 @提及) */}
<PostMentionInput
value={content}
onChangeText={setContent}
onSegmentsChange={setSegments}
placeholder="分享新鲜事..."
maxLength={MAX_CONTENT_LENGTH}
minHeight={contentInputMinHeight}
style={[
styles.contentInput,
isWideScreen && styles.contentInputWide,
{ minHeight: contentInputMinHeight },
]}
value={content}
onChangeText={setContent}
placeholder="分享新鲜事..."
placeholderTextColor={colors.text.hint}
multiline
textAlignVertical="top"
maxLength={MAX_CONTENT_LENGTH}
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
/>
{/* 作为正文容器的子模块,追加在文本后面 */}

View File

@@ -1,4 +1,4 @@
/**
/**
* 首页 HomeScreen
* 威友 - 首页展示
* 支持列表和多列网格模式(响应式布局)
@@ -27,7 +27,7 @@ import { PagerView } from '../../components/common';
import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme';
import { Post } from '../../types';
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
import { useCurrentUser } from '../../stores/auth';
import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth';
import { channelService, postService } from '../../services';
import { PostCard, TabBar, SearchBar, ShareSheet } from '../../components/business';
import type { PostCardAction } from '../../components/business/PostCard';
@@ -169,6 +169,10 @@ export const HomeScreen: React.FC = () => {
const insets = useSafeAreaInsets();
const { posts: storePosts } = useUserStore();
const currentUser = useCurrentUser();
const isAuthenticated = useIsAuthenticated();
const isVerified = useIsVerified();
const fetchVerificationStatus = useVerificationStore((s) => s.fetchStatus);
const hasFetchedVerification = useVerificationStore((s) => s.hasFetchedStatus);
const colors = useAppColors();
const resolvedScheme = useResolvedColorScheme();
const styles = useMemo(() => createHomeStyles(colors), [colors]);
@@ -207,6 +211,12 @@ export const HomeScreen: React.FC = () => {
const [showShareSheet, setShowShareSheet] = useState(false);
const [sharePost, setSharePost] = useState<Post | null>(null);
useEffect(() => {
if (isAuthenticated && !hasFetchedVerification) {
fetchVerificationStatus();
}
}, [isAuthenticated, hasFetchedVerification, fetchVerificationStatus]);
useEffect(() => {
if (showCreatePost) {
blurActiveElement();
@@ -656,6 +666,14 @@ export const HomeScreen: React.FC = () => {
// 跳转到发帖页面(使用 Modal 方式)
const handleCreatePost = () => {
if (!isAuthenticated) {
router.push('/login');
return;
}
if (!isVerified) {
router.push(hrefs.hrefVerificationGuide());
return;
}
setShowCreatePost(true);
};

View File

@@ -1,4 +1,4 @@
/**
/**
* 帖子详情页 PostDetailScreen - QQ频道风格响应式版本
* 威友 - 显示完整帖子内容和评论
* 桌面端使用双栏布局,移动端使用单栏布局
@@ -33,13 +33,13 @@ import {
useAppColors,
type AppColors,
} from '../../theme';
import { Post, Comment, VoteResultDTO, VoteOptionDTO } from '../../types';
import { Post, Comment, VoteResultDTO, VoteOptionDTO, MessageSegment } from '../../types';
import { useUserStore } from '../../stores';
import { useCurrentUser } from '../../stores/auth';
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
import { postSyncService } from '@/services/post';
import { useCursorPagination } from '../../hooks/useCursorPagination';
import { CommentItem, VoteCard, ReportDialog, ShareSheet } from '../../components/business';
import { CommentItem, VoteCard, ReportDialog, ShareSheet, PostContentRenderer, PostMentionInput } 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/core';
@@ -191,6 +191,7 @@ export const PostDetailScreen: React.FC = () => {
}
}, [commentsError, postId]);
const [commentText, setCommentText] = useState('');
const [commentSegments, setCommentSegments] = useState<MessageSegment[]>([]);
const [showImageModal, setShowImageModal] = useState(false);
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const [allImages, setAllImages] = useState<ImageGridItem[]>([]);
@@ -741,6 +742,7 @@ export const PostDetailScreen: React.FC = () => {
root_id: rootId,
target_id: parentId,
content: commentText.trim(),
segments: commentSegments.length > 0 ? commentSegments : undefined,
images: uploadedImageUrls.map(url => ({ url })),
likes_count: 0,
replies_count: 0,
@@ -798,6 +800,7 @@ export const PostDetailScreen: React.FC = () => {
const createdComment = await commentService.createComment({
postId: post.id,
content: commentText.trim(),
segments: commentSegments.length > 0 ? commentSegments : undefined,
parentId: parentId || undefined,
images: uploadedImageUrls,
});
@@ -1097,18 +1100,17 @@ export const PostDetailScreen: React.FC = () => {
{/* 内容 - 增大字体 */}
<View style={styles.contentContainer}>
<Text
variant="body"
style={[
<PostContentRenderer
content={post.content}
segments={post.segments}
textStyle={[
styles.content,
{
fontSize: isDesktop ? fontSizes.xl : isTablet ? fontSizes.lg : fontSizes.lg,
lineHeight: isDesktop ? 32 : isTablet ? 30 : 28
}
]}
>
{post.content}
</Text>
/>
</View>
{/* 图片 - 详情页显示所有图片,不限制数量 */}
@@ -1442,15 +1444,14 @@ export const PostDetailScreen: React.FC = () => {
)}
{/* Text Input */}
<TextInput
style={styles.expandedTextInput}
placeholder="来说点什么吧!"
placeholderTextColor={colors.text.hint}
<PostMentionInput
value={commentText}
onChangeText={setCommentText}
multiline
autoFocus
onSegmentsChange={setCommentSegments}
placeholder="来说点什么吧!"
maxLength={500}
minHeight={40}
style={styles.expandedTextInput}
/>
{/* Image preview */}