feat(business): enhance block editor with segment conversion and data persistence
- 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:
@@ -3,12 +3,14 @@ import { View, ScrollView, StyleSheet } from 'react-native';
|
|||||||
|
|
||||||
import { useAppColors, spacing } from '../../../theme';
|
import { useAppColors, spacing } from '../../../theme';
|
||||||
import { MessageSegment } from '../../../types';
|
import { MessageSegment } from '../../../types';
|
||||||
|
import { EditorBlock } from './blockEditorTypes';
|
||||||
import TextBlockInput from './TextBlockInput';
|
import TextBlockInput from './TextBlockInput';
|
||||||
import ImageBlockView from './ImageBlockView';
|
import ImageBlockView from './ImageBlockView';
|
||||||
import { useBlockEditor } from './useBlockEditor';
|
import { useBlockEditor } from './useBlockEditor';
|
||||||
|
|
||||||
export interface BlockEditorHandle {
|
export interface BlockEditorHandle {
|
||||||
insertImage: () => Promise<void>;
|
insertImage: () => Promise<void>;
|
||||||
|
insertCameraPhoto: () => Promise<void>;
|
||||||
insertTextAtCursor: (text: string) => void;
|
insertTextAtCursor: (text: string) => void;
|
||||||
getSegments: () => MessageSegment[];
|
getSegments: () => MessageSegment[];
|
||||||
getContent: () => string;
|
getContent: () => string;
|
||||||
@@ -22,15 +24,17 @@ interface BlockEditorProps {
|
|||||||
style?: any;
|
style?: any;
|
||||||
textStyle?: any;
|
textStyle?: any;
|
||||||
onContentChange?: (content: string) => void;
|
onContentChange?: (content: string) => void;
|
||||||
|
initialBlocks?: EditorBlock[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const BlockEditor = React.forwardRef<BlockEditorHandle, BlockEditorProps>(
|
const BlockEditor = React.forwardRef<BlockEditorHandle, BlockEditorProps>(
|
||||||
({ placeholder = '添加正文', maxLength = 2000, style, textStyle, onContentChange }, ref) => {
|
({ placeholder = '添加正文', maxLength = 2000, style, textStyle, onContentChange, initialBlocks }, ref) => {
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
const editor = useBlockEditor();
|
const editor = useBlockEditor(initialBlocks);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
insertImage: editor.insertImage,
|
insertImage: editor.insertImage,
|
||||||
|
insertCameraPhoto: editor.insertCameraPhoto,
|
||||||
insertTextAtCursor: editor.insertTextAtCursor,
|
insertTextAtCursor: editor.insertTextAtCursor,
|
||||||
getSegments: editor.getSegments,
|
getSegments: editor.getSegments,
|
||||||
getContent: editor.getContent,
|
getContent: editor.getContent,
|
||||||
|
|||||||
@@ -1,5 +1,45 @@
|
|||||||
import { MessageSegment } from '../../../types';
|
import { MessageSegment } from '../../../types';
|
||||||
import { EditorBlock, blockTextToSegments } from './blockEditorTypes';
|
import { EditorBlock, TextBlock, ImageBlock, blockTextToSegments, generateBlockId } from './blockEditorTypes';
|
||||||
|
|
||||||
|
export function segmentsToBlocks(segments: MessageSegment[]): EditorBlock[] {
|
||||||
|
if (!segments || segments.length === 0) return [];
|
||||||
|
|
||||||
|
const blocks: EditorBlock[] = [];
|
||||||
|
|
||||||
|
for (const seg of segments) {
|
||||||
|
if (seg.type === 'image' && seg.data?.url) {
|
||||||
|
const imageBlock: ImageBlock = {
|
||||||
|
id: generateBlockId(),
|
||||||
|
type: 'image',
|
||||||
|
localUri: seg.data.url,
|
||||||
|
remoteUrl: seg.data.url,
|
||||||
|
uploading: false,
|
||||||
|
width: seg.data.width,
|
||||||
|
height: seg.data.height,
|
||||||
|
};
|
||||||
|
blocks.push(imageBlock);
|
||||||
|
} else if (seg.type === 'text') {
|
||||||
|
const text = seg.data?.text || seg.data?.content || '';
|
||||||
|
if (text) {
|
||||||
|
const textBlock: TextBlock = {
|
||||||
|
id: generateBlockId(),
|
||||||
|
type: 'text',
|
||||||
|
text,
|
||||||
|
activeMentions: new Map(),
|
||||||
|
};
|
||||||
|
blocks.push(textBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Skip at/other segment types — mentions from long posts are not editable in block editor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure at least one text block
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
blocks.push({ id: generateBlockId(), type: 'text', text: '', activeMentions: new Map() });
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
export function blocksToSegments(blocks: EditorBlock[]): MessageSegment[] {
|
export function blocksToSegments(blocks: EditorBlock[]): MessageSegment[] {
|
||||||
const segments: MessageSegment[] = [];
|
const segments: MessageSegment[] = [];
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ interface MentionData {
|
|||||||
nickname: string;
|
nickname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBlockEditor() {
|
export function useBlockEditor(initialBlocks?: EditorBlock[]) {
|
||||||
const [blocks, setBlocks] = useState<EditorBlock[]>([createInitialTextBlock()]);
|
const [blocks, setBlocks] = useState<EditorBlock[]>(initialBlocks && initialBlocks.length > 0 ? initialBlocks : [createInitialTextBlock()]);
|
||||||
const [activeBlockId, setActiveBlockId] = useState<string | null>(null);
|
const [activeBlockId, setActiveBlockId] = useState<string | null>(null);
|
||||||
const inputRefs = useRef<Map<string, TextInput>>(new Map());
|
const inputRefs = useRef<Map<string, TextInput>>(new Map());
|
||||||
const blockCursors = useRef<Map<string, { start: number; end: number }>>(new Map());
|
const blockCursors = useRef<Map<string, { start: number; end: number }>>(new Map());
|
||||||
@@ -87,21 +87,56 @@ export function useBlockEditor() {
|
|||||||
});
|
});
|
||||||
}, [focusBlock]);
|
}, [focusBlock]);
|
||||||
|
|
||||||
const insertImage = useCallback(async () => {
|
const uploadImageByLocalUri = useCallback(
|
||||||
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
async (asset: ImagePicker.ImagePickerAsset) => {
|
||||||
if (!permissionResult.granted) {
|
try {
|
||||||
Alert.alert('权限不足', '需要访问相册权限来选择图片');
|
const uploadResult = await uploadService.uploadImage({
|
||||||
return;
|
uri: asset.uri,
|
||||||
}
|
type: asset.mimeType || 'image/jpeg',
|
||||||
|
});
|
||||||
|
|
||||||
const result = await ImagePicker.launchImageLibraryAsync({
|
if (uploadResult) {
|
||||||
mediaTypes: 'images',
|
setBlocks(prev =>
|
||||||
allowsMultipleSelection: true,
|
prev.map(b =>
|
||||||
selectionLimit: 0,
|
b.type === 'image' && b.localUri === asset.uri && !b.remoteUrl
|
||||||
quality: 1,
|
? {
|
||||||
});
|
...b,
|
||||||
|
remoteUrl: uploadResult.url,
|
||||||
|
uploading: false,
|
||||||
|
width: uploadResult.width ?? b.width,
|
||||||
|
height: uploadResult.height ?? b.height,
|
||||||
|
}
|
||||||
|
: b
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Upload failed
|
||||||
|
setBlocks(prev =>
|
||||||
|
prev.map(b =>
|
||||||
|
b.type === 'image' && b.localUri === asset.uri && !b.remoteUrl
|
||||||
|
? { ...b, uploading: false }
|
||||||
|
: b
|
||||||
|
)
|
||||||
|
);
|
||||||
|
Alert.alert('上传失败', '图片上传失败,请重试');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setBlocks(prev =>
|
||||||
|
prev.map(b =>
|
||||||
|
b.type === 'image' && b.localUri === asset.uri && !b.remoteUrl
|
||||||
|
? { ...b, uploading: false }
|
||||||
|
: b
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const insertImagesFromAssets = useCallback(
|
||||||
|
(assets: ImagePicker.ImagePickerAsset[]) => {
|
||||||
|
if (!assets.length) return;
|
||||||
|
|
||||||
if (!result.canceled && result.assets) {
|
|
||||||
setBlocks(prev => {
|
setBlocks(prev => {
|
||||||
let updated = [...prev];
|
let updated = [...prev];
|
||||||
let currentActiveId = activeBlockId;
|
let currentActiveId = activeBlockId;
|
||||||
@@ -111,7 +146,7 @@ export function useBlockEditor() {
|
|||||||
insertAfterIndex = updated.length - 1;
|
insertAfterIndex = updated.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const asset of result.assets) {
|
for (const asset of assets) {
|
||||||
const activeBlock = updated[insertAfterIndex];
|
const activeBlock = updated[insertAfterIndex];
|
||||||
const cursorPos = activeBlock && activeBlock.type === 'text'
|
const cursorPos = activeBlock && activeBlock.type === 'text'
|
||||||
? (blockCursors.current.get(activeBlock.id)?.start ?? activeBlock.text.length)
|
? (blockCursors.current.get(activeBlock.id)?.start ?? activeBlock.text.length)
|
||||||
@@ -170,59 +205,49 @@ export function useBlockEditor() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Start async uploads
|
// Start async uploads
|
||||||
for (const asset of result.assets) {
|
for (const asset of assets) {
|
||||||
// Find the newly created image block by localUri
|
|
||||||
uploadImageByLocalUri(asset);
|
uploadImageByLocalUri(asset);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [activeBlockId, focusBlock]);
|
|
||||||
|
|
||||||
const uploadImageByLocalUri = useCallback(
|
|
||||||
async (asset: ImagePicker.ImagePickerAsset) => {
|
|
||||||
try {
|
|
||||||
const uploadResult = await uploadService.uploadImage({
|
|
||||||
uri: asset.uri,
|
|
||||||
type: asset.mimeType || 'image/jpeg',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (uploadResult) {
|
|
||||||
setBlocks(prev =>
|
|
||||||
prev.map(b =>
|
|
||||||
b.type === 'image' && b.localUri === asset.uri && !b.remoteUrl
|
|
||||||
? {
|
|
||||||
...b,
|
|
||||||
remoteUrl: uploadResult.url,
|
|
||||||
uploading: false,
|
|
||||||
width: uploadResult.width ?? b.width,
|
|
||||||
height: uploadResult.height ?? b.height,
|
|
||||||
}
|
|
||||||
: b
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Upload failed
|
|
||||||
setBlocks(prev =>
|
|
||||||
prev.map(b =>
|
|
||||||
b.type === 'image' && b.localUri === asset.uri && !b.remoteUrl
|
|
||||||
? { ...b, uploading: false }
|
|
||||||
: b
|
|
||||||
)
|
|
||||||
);
|
|
||||||
Alert.alert('上传失败', '图片上传失败,请重试');
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setBlocks(prev =>
|
|
||||||
prev.map(b =>
|
|
||||||
b.type === 'image' && b.localUri === asset.uri && !b.remoteUrl
|
|
||||||
? { ...b, uploading: false }
|
|
||||||
: b
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[],
|
[activeBlockId, focusBlock, uploadImageByLocalUri],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const insertImage = useCallback(async () => {
|
||||||
|
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
|
if (!permissionResult.granted) {
|
||||||
|
Alert.alert('权限不足', '需要访问相册权限来选择图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
|
mediaTypes: 'images',
|
||||||
|
allowsMultipleSelection: true,
|
||||||
|
selectionLimit: 0,
|
||||||
|
quality: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled && result.assets) {
|
||||||
|
insertImagesFromAssets(result.assets);
|
||||||
|
}
|
||||||
|
}, [insertImagesFromAssets]);
|
||||||
|
|
||||||
|
const insertCameraPhoto = useCallback(async () => {
|
||||||
|
const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
|
||||||
|
if (!permissionResult.granted) {
|
||||||
|
Alert.alert('权限不足', '需要访问相机权限来拍照');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ImagePicker.launchCameraAsync({
|
||||||
|
allowsEditing: true,
|
||||||
|
quality: 0.8,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled && result.assets && result.assets[0]) {
|
||||||
|
insertImagesFromAssets([result.assets[0]]);
|
||||||
|
}
|
||||||
|
}, [insertImagesFromAssets]);
|
||||||
|
|
||||||
const insertTextAtCursor = useCallback((text: string) => {
|
const insertTextAtCursor = useCallback((text: string) => {
|
||||||
const activeId = activeBlockId;
|
const activeId = activeBlockId;
|
||||||
if (!activeId) return;
|
if (!activeId) return;
|
||||||
@@ -278,6 +303,7 @@ export function useBlockEditor() {
|
|||||||
updateTextBlock,
|
updateTextBlock,
|
||||||
removeImageBlock,
|
removeImageBlock,
|
||||||
insertImage,
|
insertImage,
|
||||||
|
insertCameraPhoto,
|
||||||
insertTextAtCursor,
|
insertTextAtCursor,
|
||||||
focusBlock,
|
focusBlock,
|
||||||
getSegments,
|
getSegments,
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import { uploadService } from '@/services/upload';
|
|||||||
import VoteEditor from '../../components/business/VoteEditor';
|
import VoteEditor from '../../components/business/VoteEditor';
|
||||||
import PostMentionInput from '../../components/business/PostMentionInput';
|
import PostMentionInput from '../../components/business/PostMentionInput';
|
||||||
import BlockEditor, { BlockEditorHandle } from '../../components/business/BlockEditor';
|
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 { useResponsive, useResponsiveValue } from '../../hooks';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
import { MessageSegment } from '../../types';
|
import { MessageSegment } from '../../types';
|
||||||
@@ -138,6 +140,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
|||||||
|
|
||||||
// 长文模式:图片作为 image segment 嵌入文本
|
// 长文模式:图片作为 image segment 嵌入文本
|
||||||
const [isLongPostMode, setIsLongPostMode] = useState(false);
|
const [isLongPostMode, setIsLongPostMode] = useState(false);
|
||||||
|
const [initialBlocks, setInitialBlocks] = useState<EditorBlock[] | undefined>(undefined);
|
||||||
|
|
||||||
// 投票相关状态
|
// 投票相关状态
|
||||||
const [isVotePost, setIsVotePost] = useState(false);
|
const [isVotePost, setIsVotePost] = useState(false);
|
||||||
@@ -211,6 +214,13 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
|||||||
setTitle(existingPost.title || '');
|
setTitle(existingPost.title || '');
|
||||||
setContent(existingPost.content || '');
|
setContent(existingPost.content || '');
|
||||||
setImages((existingPost.images || []).map((img) => ({ uri: img.url, uploading: false })));
|
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) {
|
} catch (error) {
|
||||||
console.error('加载待编辑帖子失败:', error);
|
console.error('加载待编辑帖子失败:', error);
|
||||||
Alert.alert('错误', '加载帖子失败,请稍后重试');
|
Alert.alert('错误', '加载帖子失败,请稍后重试');
|
||||||
@@ -337,6 +347,33 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
|||||||
setShowChannelPicker(false);
|
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 handleToggleEmojiPanel = () => {
|
||||||
const willShow = !showEmojiPanel;
|
const willShow = !showEmojiPanel;
|
||||||
@@ -666,6 +703,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
|||||||
isWideScreen && styles.contentInputWide,
|
isWideScreen && styles.contentInputWide,
|
||||||
]}
|
]}
|
||||||
onContentChange={setContent}
|
onContentChange={setContent}
|
||||||
|
initialBlocks={initialBlocks}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -751,21 +789,19 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
|||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{!isLongPostMode && (
|
<TouchableOpacity
|
||||||
<TouchableOpacity
|
style={styles.toolbarButton}
|
||||||
style={styles.toolbarButton}
|
onPress={isLongPostMode ? () => blockEditorRef.current?.insertCameraPhoto() : handleTakePhoto}
|
||||||
onPress={handleTakePhoto}
|
disabled={posting}
|
||||||
disabled={posting}
|
>
|
||||||
>
|
<View style={styles.toolbarButtonInner}>
|
||||||
<View style={styles.toolbarButtonInner}>
|
<MaterialCommunityIcons
|
||||||
<MaterialCommunityIcons
|
name="camera-outline"
|
||||||
name="camera-outline"
|
size={24}
|
||||||
size={24}
|
color={colors.text.secondary}
|
||||||
color={colors.text.secondary}
|
/>
|
||||||
/>
|
</View>
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.toolbarButton}
|
style={styles.toolbarButton}
|
||||||
@@ -799,7 +835,7 @@ export const CreatePostScreen: React.FC<CreatePostScreenProps> = (props) => {
|
|||||||
{/* 长文模式开关 */}
|
{/* 长文模式开关 */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.toolbarButton}
|
style={styles.toolbarButton}
|
||||||
onPress={() => setIsLongPostMode(prev => !prev)}
|
onPress={handleToggleLongPostMode}
|
||||||
disabled={posting}
|
disabled={posting}
|
||||||
>
|
>
|
||||||
<View style={styles.toolbarButtonInner}>
|
<View style={styles.toolbarButtonInner}>
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ function createAboutStyles(colors: AppColors) {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: colors.text.hint,
|
color: colors.text.hint,
|
||||||
marginTop: spacing.xs,
|
marginTop: spacing.xs,
|
||||||
|
textDecorationLine: 'underline',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -571,7 +572,7 @@ export const AboutScreen: React.FC = () => {
|
|||||||
|
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Text style={styles.copyright}>© 2026 青春之旅电子信息科技(威海)有限公司</Text>
|
<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>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
UserModel,
|
UserModel,
|
||||||
GroupModel,
|
GroupModel,
|
||||||
MessageModel,
|
MessageModel,
|
||||||
} from '../models';
|
} from '../../data/models';
|
||||||
import type {
|
import type {
|
||||||
ConversationResponse,
|
ConversationResponse,
|
||||||
ConversationDetailResponse,
|
ConversationDetailResponse,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
MessageModel,
|
MessageModel,
|
||||||
MessageSegment,
|
MessageSegment,
|
||||||
UserModel,
|
UserModel,
|
||||||
} from '../models';
|
} from '../../data/models';
|
||||||
import type {
|
import type {
|
||||||
MessageResponse,
|
MessageResponse,
|
||||||
UserDTO,
|
UserDTO,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 负责 Post 模型与 API 响应之间的转换
|
* 负责 Post 模型与 API 响应之间的转换
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PostModel, UserModel } from '../models';
|
import { PostModel, UserModel } from '../../data/models';
|
||||||
import type { PostDTO } from '../../types/dto';
|
import type { PostDTO } from '../../types/dto';
|
||||||
|
|
||||||
export class PostMapper {
|
export class PostMapper {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 负责 User 模型与 API 响应、数据库记录之间的转换
|
* 负责 User 模型与 API 响应、数据库记录之间的转换
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { UserModel } from '../models';
|
import { UserModel } from '../../data/models';
|
||||||
import type { UserDTO } from '../../types/dto';
|
import type { UserDTO } from '../../types/dto';
|
||||||
|
|
||||||
// 数据库用户记录类型
|
// 数据库用户记录类型
|
||||||
|
|||||||
Reference in New Issue
Block a user