feat(business): enhance block editor with segment conversion and data persistence
Some checks failed
Frontend CI / ota-android (push) Successful in 1m17s
Frontend CI / ota-ios (push) Successful in 1m32s
Frontend CI / build-and-push-web (push) Successful in 3m5s
Frontend CI / build-android-apk (push) Failing after 3h12m39s

- Implement `segmentsToBlocks` utility to convert message segments into editable blocks.
- Update `BlockEditor` and `useBlockEditor` to support initial block loading and improved image upload handling.
- Add logic to `CreatePostScreen` to automatically switch to long-post mode when image segments are detected and preserve data when toggling modes.
- Update `AboutScreen` to make the ICP filing number clickable.
- Refactor mapper imports to use the updated data model paths.
This commit is contained in:
2026-05-08 13:19:40 +08:00
parent d4c3e1f268
commit 81ee9ba75b
9 changed files with 196 additions and 89 deletions

View File

@@ -42,6 +42,8 @@ import { uploadService } from '@/services/upload';
import VoteEditor from '../../components/business/VoteEditor';
import PostMentionInput from '../../components/business/PostMentionInput';
import BlockEditor, { BlockEditorHandle } from '../../components/business/BlockEditor';
import { segmentsToBlocks } from '../../components/business/BlockEditor/blocksToSegments';
import type { EditorBlock } from '../../components/business/BlockEditor/blockEditorTypes';
import { useResponsive, useResponsiveValue } from '../../hooks';
import * as hrefs from '../../navigation/hrefs';
import { MessageSegment } from '../../types';
@@ -135,9 +137,10 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
const [showEmojiPanel, setShowEmojiPanel] = useState(false);
const [posting, setPosting] = useState(false);
const [loadingPost, setLoadingPost] = useState(false);
// 长文模式:图片作为 image segment 嵌入文本
const [isLongPostMode, setIsLongPostMode] = useState(false);
const [initialBlocks, setInitialBlocks] = useState<EditorBlock[] | undefined>(undefined);
// 投票相关状态
const [isVotePost, setIsVotePost] = useState(false);
@@ -211,6 +214,13 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
setTitle(existingPost.title || '');
setContent(existingPost.content || '');
setImages((existingPost.images || []).map((img) => ({ uri: img.url, uploading: false })));
// 自动判断长文模式segments 中包含图片块即为长文帖子
const hasImageSegments = (existingPost.segments || []).some(s => s.type === 'image');
if (hasImageSegments) {
setIsLongPostMode(true);
setInitialBlocks(segmentsToBlocks(existingPost.segments || []));
}
} catch (error) {
console.error('加载待编辑帖子失败:', error);
Alert.alert('错误', '加载帖子失败,请稍后重试');
@@ -337,6 +347,33 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
setShowChannelPicker(false);
};
// 切换长文/非长文模式时保留数据
const handleToggleLongPostMode = useCallback(() => {
if (!isLongPostMode) {
// 非长文 -> 长文:将当前文本和图片转为 blocks
const segs: MessageSegment[] = [];
if (content.trim()) {
segs.push({ type: 'text', data: { text: content } });
}
for (const img of images) {
if (img.uri && !img.uploading) {
segs.push({ type: 'image', data: { url: img.uri } });
}
}
setInitialBlocks(segs.length > 0 ? segmentsToBlocks(segs) : undefined);
} else {
// 长文 -> 非长文:从 BlockEditor 提取文本和图片
const editorContent = blockEditorRef.current?.getContent() || '';
const editorSegments = blockEditorRef.current?.getSegments() || [];
if (editorContent) setContent(editorContent);
const editorImages = editorSegments
.filter((s): s is Extract<typeof s, { type: 'image' }> => s.type === 'image')
.map(s => ({ uri: s.data.url, uploading: false }));
if (editorImages.length > 0) setImages(editorImages);
}
setIsLongPostMode(prev => !prev);
}, [isLongPostMode, content, images]);
// 切换表情面板
const handleToggleEmojiPanel = () => {
const willShow = !showEmojiPanel;
@@ -666,6 +703,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
isWideScreen && styles.contentInputWide,
]}
onContentChange={setContent}
initialBlocks={initialBlocks}
/>
) : (
<>
@@ -751,21 +789,19 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
</View>
</TouchableOpacity>
{!isLongPostMode && (
<TouchableOpacity
style={styles.toolbarButton}
onPress={handleTakePhoto}
disabled={posting}
>
<View style={styles.toolbarButtonInner}>
<MaterialCommunityIcons
name="camera-outline"
size={24}
color={colors.text.secondary}
/>
</View>
</TouchableOpacity>
)}
<TouchableOpacity
style={styles.toolbarButton}
onPress={isLongPostMode ? () => blockEditorRef.current?.insertCameraPhoto() : handleTakePhoto}
disabled={posting}
>
<View style={styles.toolbarButtonInner}>
<MaterialCommunityIcons
name="camera-outline"
size={24}
color={colors.text.secondary}
/>
</View>
</TouchableOpacity>
<TouchableOpacity
style={styles.toolbarButton}
@@ -799,7 +835,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
{/* 长文模式开关 */}
<TouchableOpacity
style={styles.toolbarButton}
onPress={() => setIsLongPostMode(prev => !prev)}
onPress={handleToggleLongPostMode}
disabled={posting}
>
<View style={styles.toolbarButtonInner}>

View File

@@ -300,6 +300,7 @@ function createAboutStyles(colors: AppColors) {
fontSize: 12,
color: colors.text.hint,
marginTop: spacing.xs,
textDecorationLine: 'underline',
},
});
}
@@ -571,7 +572,7 @@ export const AboutScreen: React.FC = () => {
<View style={styles.footer}>
<Text style={styles.copyright}>© 2026 </Text>
<Text style={styles.companyName}>ICP备XXXXXXXX-1</Text>
<Text style={styles.companyName} onPress={() => Linking.openURL('https://beian.miit.gov.cn/')}>ICP备2025144569-2A</Text>
</View>
</View>
</ScrollView>