Files
frontend/src/screens/create/CreatePostScreen.tsx
lan 3968660048 Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files.

Made-with: Cursor
2026-03-09 21:29:03 +08:00

940 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 发帖页 CreatePostScreen响应式适配
* 胡萝卜BBS - 发布新帖子
* 参考微博发帖界面设计
* 表单在宽屏下居中显示
* 图片选择器在宽屏下显示更大的预览
* 投票编辑器在宽屏下优化布局
*/
import React, { useState, useCallback } from 'react';
import {
View,
ScrollView,
StyleSheet,
TouchableOpacity,
TextInput,
Alert,
KeyboardAvoidingView,
Platform,
Animated,
Image,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
import { Text, Button, ResponsiveContainer } from '../../components/common';
import { postService, showPrompt, voteService } from '../../services';
import { ApiError } from '../../services/api';
import { uploadService } from '../../services/uploadService';
import VoteEditor from '../../components/business/VoteEditor';
import { useResponsive, useResponsiveValue } from '../../hooks';
const MAX_TITLE_LENGTH = 100;
const MAX_CONTENT_LENGTH = 2000;
// 表情面板高度
const EMOJI_PANEL_HEIGHT = 280;
// 常用表情列表
const EMOJIS = [
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂',
'🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩',
'😘', '😗', '😚', '😙', '🥲', '😋', '😛', '😜',
'🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐',
'🤨', '😐', '😑', '😶', '😏', '😒', '🙄',
'😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷',
'🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴',
'😵', '🤯', '🤠', '🥳', '🥸', '😎', '🤓',
'🧐', '😕', '😟', '🙁', '☹️', '😮', '😯', '😲',
'😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢',
'😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫',
'🥱', '😤', '😡', '😠', '🤬', '😈', '👿', '💀',
'👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏',
'✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆',
'👍', '👎', '✊', '👊', '🤛', '🤜', '👏', '🙌',
'👐', '🤲', '🤝', '🙏', '✍️', '💪', '🦾', '🦵',
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍',
'🤎', '💔', '❤️\u200d🔥', '❤️\u200d🩹', '💕', '💞', '💓', '💗',
'💖', '💘', '💝', '🎉', '🎊', '🎁', '🎈', '✨',
'🔥', '💯', '💢', '💥', '💫', '💦', '💨', '🕳️',
];
// 动画值
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
const getPublishErrorMessage = (error: unknown): string => {
if (error instanceof ApiError && error.message?.trim()) {
return error.message.trim();
}
return '发布失败,请重试';
};
export const CreatePostScreen: React.FC = () => {
const navigation = useNavigation();
// 响应式布局
const { isWideScreen, width } = useResponsive();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [images, setImages] = useState<{ uri: string; uploading?: boolean }[]>([]);
const [tags, setTags] = useState<string[]>([]);
const [tagInput, setTagInput] = useState('');
const [showTagInput, setShowTagInput] = useState(false);
const [showEmojiPanel, setShowEmojiPanel] = useState(false);
const [posting, setPosting] = useState(false);
// 投票相关状态
const [isVotePost, setIsVotePost] = useState(false);
const [voteOptions, setVoteOptions] = useState<string[]>(['', '']); // 默认2个选项
// 内容输入框引用
const contentInputRef = React.useRef<TextInput>(null);
const [selection, setSelection] = useState({ start: 0, end: 0 });
// 动画值
const fadeAnim = React.useRef(new Animated.Value(0)).current;
const slideAnim = React.useRef(new Animated.Value(20)).current;
// 响应式图片网格配置
const imagesPerRow = useResponsiveValue({ xs: 3, sm: 3, md: 4, lg: 5, xl: 6 });
const imageGap = 8;
const availableWidth = isWideScreen
? Math.min(width, 800) - spacing.lg * 2
: width - spacing.lg * 2;
const imageSize = (availableWidth - imageGap * (imagesPerRow - 1)) / imagesPerRow;
React.useEffect(() => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 250,
useNativeDriver: true,
}),
]).start();
}, []);
// 选择图片
const handlePickImage = 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) {
const newImages = result.assets.map(asset => ({ uri: asset.uri, uploading: true }));
setImages(prev => [...prev, ...newImages]);
// 上传图片
for (let i = 0; i < newImages.length; i++) {
const asset = result.assets[i];
const uploadResult = await uploadService.uploadImage({
uri: asset.uri,
type: asset.mimeType || 'image/jpeg',
});
if (uploadResult) {
setImages(prev => {
const updated = [...prev];
const index = prev.findIndex(img => img.uri === asset.uri);
if (index !== -1) {
updated[index] = { uri: uploadResult.url, uploading: false };
}
return updated;
});
} else {
Alert.alert('上传失败', '图片上传失败,请重试');
setImages(prev => prev.filter(img => img.uri !== asset.uri));
}
}
}
};
// 拍照
const handleTakePhoto = 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[0]) {
const asset = result.assets[0];
setImages(prev => [...prev, { uri: asset.uri, uploading: true }]);
// 上传图片
const uploadResult = await uploadService.uploadImage({
uri: asset.uri,
type: asset.mimeType || 'image/jpeg',
});
if (uploadResult) {
setImages(prev => {
const updated = [...prev];
const index = prev.findIndex(img => img.uri === asset.uri);
if (index !== -1) {
updated[index] = { uri: uploadResult.url, uploading: false };
}
return updated;
});
} else {
Alert.alert('上传失败', '图片上传失败,请重试');
setImages(prev => prev.filter(img => img.uri !== asset.uri));
}
}
};
// 删除图片
const handleRemoveImage = (index: number) => {
setImages(images.filter((_, i) => i !== index));
};
// 添加标签
const handleAddTag = useCallback(() => {
const tag = tagInput.trim().replace(/^#/, '');
if (tag && !tags.includes(tag) && tags.length < 5) {
setTags([...tags, tag]);
setTagInput('');
setShowTagInput(false);
}
}, [tagInput, tags]);
// 删除标签
const handleRemoveTag = (tag: string) => {
setTags(tags.filter(t => t !== tag));
};
// 插入表情
const handleInsertEmoji = (emoji: string) => {
const newContent = content.slice(0, selection.start) + emoji + content.slice(selection.end);
setContent(newContent);
const newPosition = selection.start + emoji.length;
setSelection({ start: newPosition, end: newPosition });
};
// 关闭所有面板
const closeAllPanels = () => {
setShowEmojiPanel(false);
setShowTagInput(false);
};
// 切换表情面板
const handleToggleEmojiPanel = () => {
closeAllPanels();
setShowEmojiPanel(!showEmojiPanel);
};
// 切换标签输入
const handleToggleTagInput = () => {
closeAllPanels();
setShowTagInput(!showTagInput);
};
// 切换投票模式
const handleToggleVote = () => {
closeAllPanels();
setIsVotePost(!isVotePost);
};
// 添加投票选项
const handleAddVoteOption = () => {
if (voteOptions.length < 10) {
setVoteOptions([...voteOptions, '']);
}
};
// 删除投票选项
const handleRemoveVoteOption = (index: number) => {
if (voteOptions.length > 2) {
setVoteOptions(voteOptions.filter((_, i) => i !== index));
}
};
// 更新投票选项
const handleUpdateVoteOption = (index: number, value: string) => {
const newOptions = [...voteOptions];
newOptions[index] = value;
setVoteOptions(newOptions);
};
// 发布帖子
const handlePost = async () => {
if (!content.trim()) {
Alert.alert('错误', '请输入帖子内容');
return;
}
// 检查是否有图片正在上传
const uploadingImages = images.filter(img => img.uploading);
if (uploadingImages.length > 0) {
Alert.alert('请稍候', '图片正在上传中,请稍后再试');
return;
}
setPosting(true);
try {
const imageUrls = images.map(img => img.uri);
if (isVotePost) {
// 验证投票选项
const validOptions = voteOptions.filter(opt => opt.trim() !== '');
if (validOptions.length < 2) {
Alert.alert('错误', '投票选项至少需要2个');
setPosting(false);
return;
}
// 创建投票帖子
await voteService.createVotePost({
title: title.trim() || '无标题',
content: content.trim(),
images: imageUrls,
vote_options: validOptions,
});
showPrompt({
type: 'info',
title: '审核中',
message: '投票帖已提交,内容审核中,稍后展示',
duration: 2600,
});
navigation.goBack();
} else {
// 创建普通帖子
await postService.createPost({
title: title.trim() || '无标题',
content: content.trim(),
images: imageUrls,
});
showPrompt({
type: 'info',
title: '审核中',
message: '帖子已提交,内容审核中,稍后展示',
duration: 2600,
});
navigation.goBack();
}
} catch (error) {
console.error('发布帖子失败:', error);
Alert.alert('错误', getPublishErrorMessage(error));
} finally {
setPosting(false);
}
};
// 渲染图片网格
const renderImageGrid = () => {
if (images.length === 0) return null;
return (
<View style={styles.imageGrid}>
{images.map((img, index) => (
<Animated.View
key={`${img.uri}-${index}`}
style={[
styles.imageGridItem,
{
width: imageSize,
height: imageSize,
opacity: fadeAnim,
transform: [{ scale: fadeAnim }]
}
]}
>
<Image source={{ uri: img.uri }} style={styles.gridImage} resizeMode="cover" />
{img.uploading && (
<View style={styles.uploadingOverlay}>
<MaterialCommunityIcons name="loading" size={20} color={colors.text.inverse} />
</View>
)}
<TouchableOpacity
style={styles.removeImageButton}
onPress={() => handleRemoveImage(index)}
disabled={img.uploading}
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
>
<View style={styles.removeImageButtonInner}>
<MaterialCommunityIcons name="close" size={12} color={colors.text.inverse} />
</View>
</TouchableOpacity>
</Animated.View>
))}
{/* 添加图片按钮 */}
<TouchableOpacity
style={[
styles.addImageGridButton,
{ width: imageSize, height: imageSize }
]}
onPress={handlePickImage}
>
<MaterialCommunityIcons name="plus" size={32} color={colors.text.hint} />
</TouchableOpacity>
</View>
);
};
// 渲染标签
const renderTags = () => {
if (tags.length === 0 && !showTagInput) return null;
return (
<View style={styles.tagsSection}>
<View style={styles.tagsContainer}>
{tags.map((tag, index) => (
<View key={index} style={styles.tag}>
<MaterialCommunityIcons name="pound" size={12} color={colors.primary.main} />
<Text variant="caption" color={colors.primary.main} style={styles.tagText}>{tag}</Text>
{/* 独立的删除按钮 */}
<TouchableOpacity
style={styles.tagDeleteButton}
onPress={() => handleRemoveTag(tag)}
hitSlop={{ top: 5, right: 5, bottom: 5, left: 5 }}
>
<MaterialCommunityIcons name="close-circle" size={14} color={colors.primary.main} />
</TouchableOpacity>
</View>
))}
{tags.length < 5 && showTagInput && (
<View style={styles.tagInputWrapper}>
<MaterialCommunityIcons name="pound" size={14} color={colors.primary.main} />
<TextInput
style={styles.tagInput}
value={tagInput}
onChangeText={setTagInput}
placeholder="输入话题"
placeholderTextColor={colors.text.hint}
onSubmitEditing={handleAddTag}
returnKeyType="done"
autoFocus
maxLength={20}
/>
<TouchableOpacity onPress={() => { setTagInput(''); setShowTagInput(false); }} style={styles.tagCancelButton}>
<MaterialCommunityIcons name="close" size={16} color={colors.text.secondary} />
</TouchableOpacity>
<TouchableOpacity onPress={handleAddTag} style={styles.tagConfirmButton}>
<MaterialCommunityIcons name="check" size={16} color={colors.primary.main} />
</TouchableOpacity>
</View>
)}
{tags.length < 5 && !showTagInput && (
<TouchableOpacity style={styles.addTagButton} onPress={() => setShowTagInput(true)}>
<MaterialCommunityIcons name="plus" size={14} color={colors.text.secondary} />
<Text variant="caption" color={colors.text.secondary} style={styles.addTagText}></Text>
</TouchableOpacity>
)}
</View>
</View>
);
};
// 渲染投票编辑器
const renderVoteEditor = () => {
if (!isVotePost) return null;
return (
<View style={isWideScreen && styles.voteEditorWide}>
<VoteEditor
options={voteOptions}
onAddOption={handleAddVoteOption}
onRemoveOption={handleRemoveVoteOption}
onUpdateOption={handleUpdateVoteOption}
maxOptions={10}
minOptions={2}
maxLength={50}
/>
</View>
);
};
// 渲染内容输入区
const renderContentSection = () => (
<View style={styles.contentSection}>
{/* 标题输入 */}
<TextInput
style={[
styles.titleInput,
isWideScreen && styles.titleInputWide,
]}
value={title}
onChangeText={setTitle}
placeholder="添加标题(可选)"
placeholderTextColor={colors.text.hint}
maxLength={MAX_TITLE_LENGTH}
/>
{/* 正文输入 */}
<TextInput
ref={contentInputRef}
style={[
styles.contentInput,
isWideScreen && styles.contentInputWide,
]}
value={content}
onChangeText={setContent}
placeholder="分享新鲜事..."
placeholderTextColor={colors.text.hint}
multiline
textAlignVertical="top"
maxLength={MAX_CONTENT_LENGTH}
onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
/>
</View>
);
// 渲染表情面板
const renderEmojiPanel = () => {
if (!showEmojiPanel) return null;
return (
<View style={[
styles.emojiPanel,
isWideScreen && styles.emojiPanelWide,
]}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.emojiGrid}
>
{EMOJIS.map((emoji, index) => (
<TouchableOpacity
key={index}
style={styles.emojiItem}
onPress={() => handleInsertEmoji(emoji)}
>
<Text style={styles.emojiText}>{emoji}</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
);
};
// 渲染底部工具栏
const renderToolbar = () => (
<View style={styles.toolbar}>
{/* 左侧功能按钮组 */}
<View style={styles.toolbarLeft}>
<TouchableOpacity
style={styles.toolbarButton}
onPress={handlePickImage}
disabled={posting}
>
<View style={styles.toolbarButtonInner}>
<MaterialCommunityIcons
name="image-outline"
size={24}
color={colors.text.secondary}
/>
</View>
</TouchableOpacity>
<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={handleToggleTagInput}
disabled={tags.length >= 5 || posting}
>
<View style={styles.toolbarButtonInner}>
<MaterialCommunityIcons
name="pound"
size={24}
color={tags.length >= 5 ? colors.text.disabled : (showTagInput ? colors.primary.main : colors.text.secondary)}
/>
{tags.length > 0 && (
<View style={styles.tagBadge}>
<Text style={styles.tagBadgeText}>{tags.length}</Text>
</View>
)}
</View>
</TouchableOpacity>
<TouchableOpacity
style={styles.toolbarButton}
onPress={handleToggleEmojiPanel}
disabled={posting}
>
<View style={styles.toolbarButtonInner}>
<MaterialCommunityIcons
name={showEmojiPanel ? "emoticon-happy" : "emoticon-happy-outline"}
size={24}
color={showEmojiPanel ? colors.primary.main : colors.text.secondary}
/>
</View>
</TouchableOpacity>
{/* 投票按钮 */}
<TouchableOpacity
style={styles.toolbarButton}
onPress={handleToggleVote}
disabled={posting}
>
<View style={styles.toolbarButtonInner}>
<MaterialCommunityIcons
name={isVotePost ? "vote" : "vote-outline"}
size={24}
color={isVotePost ? colors.primary.main : colors.text.secondary}
/>
</View>
</TouchableOpacity>
</View>
{/* 右侧发布按钮 */}
<View style={styles.toolbarRight}>
<Text variant="caption" color={colors.text.hint} style={styles.charCount}>
{content.length}/{MAX_CONTENT_LENGTH}
</Text>
<TouchableOpacity
style={[
styles.postButton,
(!content.trim() || posting) && styles.postButtonDisabled
]}
onPress={handlePost}
disabled={!content.trim() || posting}
activeOpacity={0.8}
>
{posting ? (
<MaterialCommunityIcons name="loading" size={18} color={colors.primary.contrast} />
) : (
<Text variant="body" color={colors.primary.contrast} style={styles.postButtonText}>
</Text>
)}
</TouchableOpacity>
</View>
</View>
);
// 渲染主内容
const renderMainContent = () => (
<>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* 内容输入区 */}
{renderContentSection()}
{/* 投票编辑器 */}
{renderVoteEditor()}
{/* 图片网格 */}
{renderImageGrid()}
{/* 标签 */}
{renderTags()}
</ScrollView>
{/* 表情面板 - 位于底部工具栏上方 */}
{renderEmojiPanel()}
{/* 底部工具栏 - 重新设计 */}
{renderToolbar()}
</>
);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<KeyboardAvoidingView
style={styles.flex}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
>
{isWideScreen ? (
<ResponsiveContainer maxWidth={800}>
{renderMainContent()}
</ResponsiveContainer>
) : (
renderMainContent()
)}
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
flex: {
flex: 1,
},
container: {
flex: 1,
backgroundColor: colors.background.paper,
},
scrollContent: {
paddingBottom: spacing.xl,
},
// 内容输入区
contentSection: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.md,
},
titleInput: {
fontSize: fontSizes.lg,
fontWeight: '600',
color: colors.text.primary,
paddingVertical: spacing.sm,
marginBottom: spacing.sm,
},
titleInputWide: {
fontSize: fontSizes.xl,
},
contentInput: {
fontSize: fontSizes.md,
color: colors.text.primary,
minHeight: 120,
lineHeight: 22,
paddingVertical: spacing.sm,
},
contentInputWide: {
fontSize: fontSizes.lg,
lineHeight: 26,
minHeight: 150,
},
// 图片网格
imageGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
paddingHorizontal: spacing.lg,
paddingTop: spacing.md,
gap: 8,
},
imageGridItem: {
borderRadius: borderRadius.md,
overflow: 'hidden',
backgroundColor: colors.background.disabled,
},
gridImage: {
width: '100%',
height: '100%',
},
uploadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
removeImageButton: {
position: 'absolute',
top: 4,
right: 4,
zIndex: 10,
},
removeImageButtonInner: {
width: 20,
height: 20,
borderRadius: borderRadius.full,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
justifyContent: 'center',
alignItems: 'center',
},
addImageGridButton: {
borderRadius: borderRadius.md,
borderWidth: 1,
borderColor: colors.divider,
borderStyle: 'dashed',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.default,
},
// 标签
tagsSection: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.lg,
},
tagsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.sm,
},
tag: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary.light + '15',
borderRadius: borderRadius.full,
paddingLeft: spacing.sm,
paddingRight: spacing.xs,
paddingVertical: spacing.xs,
},
tagText: {
marginLeft: spacing.xs,
fontWeight: '500',
},
tagDeleteButton: {
marginLeft: spacing.xs,
padding: 2,
},
tagInputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: borderRadius.full,
paddingLeft: spacing.sm,
paddingRight: spacing.xs,
paddingVertical: spacing.xs,
borderWidth: 1,
borderColor: colors.primary.main,
},
tagInput: {
fontSize: fontSizes.sm,
color: colors.text.primary,
minWidth: 80,
marginLeft: spacing.xs,
padding: 0,
},
tagCancelButton: {
padding: spacing.xs,
marginRight: spacing.xs,
},
tagConfirmButton: {
padding: spacing.xs,
},
addTagButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: borderRadius.full,
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderWidth: 1,
borderColor: colors.divider,
borderStyle: 'dashed',
},
addTagText: {
marginLeft: spacing.xs,
},
// 投票编辑器宽屏样式
voteEditorWide: {
maxWidth: 600,
alignSelf: 'center',
width: '100%',
},
// 底部工具栏
toolbar: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm,
backgroundColor: colors.background.paper,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.divider,
},
toolbarLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
toolbarRight: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.md,
},
toolbarButton: {
position: 'relative',
},
toolbarButtonInner: {
width: 40,
height: 40,
borderRadius: borderRadius.full,
justifyContent: 'center',
alignItems: 'center',
},
tagBadge: {
position: 'absolute',
top: 4,
right: 4,
minWidth: 16,
height: 16,
borderRadius: 8,
backgroundColor: colors.primary.main,
justifyContent: 'center',
alignItems: 'center',
},
tagBadgeText: {
color: colors.primary.contrast,
fontSize: fontSizes.xs,
fontWeight: '600',
},
charCount: {
fontSize: fontSizes.xs,
},
postButton: {
backgroundColor: colors.primary.main,
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
minWidth: 64,
alignItems: 'center',
justifyContent: 'center',
},
postButtonDisabled: {
backgroundColor: colors.text.disabled,
},
postButtonText: {
fontWeight: '600',
fontSize: fontSizes.sm,
},
// 表情面板
emojiPanel: {
height: EMOJI_PANEL_HEIGHT,
backgroundColor: colors.background.paper,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.divider,
},
emojiPanelWide: {
height: 320,
},
emojiGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
padding: spacing.md,
justifyContent: 'flex-start',
},
emojiItem: {
width: '12.5%',
aspectRatio: 1,
justifyContent: 'center',
alignItems: 'center',
},
emojiText: {
fontSize: 24,
},
});
export default CreatePostScreen;