调整emoji布局,硬编码改为flex
Some checks failed
Frontend CI / ota-android (pull_request) Has been skipped
Frontend CI / build-android-apk (pull_request) Failing after 37s
Frontend CI / build-and-push-web (pull_request) Successful in 2m53s

This commit is contained in:
2026-03-27 23:05:53 +08:00
parent d8bc3636cf
commit 8ce4e1e6da
2 changed files with 213 additions and 28 deletions

View File

@@ -52,6 +52,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
// 响应式布局
const isWideScreen = useBreakpointGTE('lg');
const isTablet = useBreakpointGTE('md');
const { width: screenWidth } = useResponsive();
// 获取表情尺寸
const emojiSize = useMemo(() => {
@@ -60,12 +61,14 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
return EMOJI_SIZES.mobile;
}, [isWideScreen, isTablet]);
// 表情网格列数
// 表情网格列数 - 根据实际屏幕宽度动态计算
const emojiColumns = useMemo(() => {
if (isWideScreen) return 10;
if (isTablet) return 9;
return 8;
}, [isWideScreen, isTablet]);
// 根据屏幕宽度计算列数每个表情项约44px宽包含间距
const itemWidth = emojiSize.itemHeight;
const calculatedColumns = Math.floor(screenWidth / itemWidth);
// 限制最小和最大列数
return Math.max(8, Math.min(calculatedColumns, 20));
}, [screenWidth, emojiSize.itemHeight]);
// 合并样式
const styles = useMemo(() => {
@@ -104,36 +107,31 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
}
}, [activeTab, loadStickers]);
const renderEmojiItem: ListRenderItem<string> = useCallback(({ item }) => (
// 渲染单个emoji
const renderEmojiButton = (emoji: string, index: number) => (
<TouchableOpacity
style={styles.emojiItem}
onPress={() => onInsertEmoji(item)}
key={`${emoji}-${index}`}
style={[styles.emojiItem, { width: 40, margin: 4 }]}
onPress={() => onInsertEmoji(emoji)}
activeOpacity={0.7}
>
<Text style={styles.emojiText}>{item}</Text>
<Text style={styles.emojiText}>{emoji}</Text>
</TouchableOpacity>
), [styles.emojiItem, styles.emojiText, onInsertEmoji]);
);
// 渲染 Emoji 面板(使用 FlatList 虚拟化,降低首次打开卡顿)
// 渲染 Emoji 面板 - 使用ScrollView包裹实现滚动
const renderEmojiPanel = () => (
<FlatList
data={EMOJIS}
key={emojiColumns}
numColumns={emojiColumns}
keyExtractor={(item, index) => `${item}-${index}`}
renderItem={renderEmojiItem}
contentContainerStyle={styles.emojiGrid}
showsVerticalScrollIndicator={false}
renderItem={({ item, index }) => renderEmojiButton(item, index)}
contentContainerStyle={[styles.emojiGrid, { flexDirection: 'row', flexWrap: 'wrap', padding: spacing.sm }]}
showsVerticalScrollIndicator={true}
keyboardShouldPersistTaps="handled"
initialNumToRender={emojiColumns * 4}
maxToRenderPerBatch={emojiColumns * 4}
windowSize={7}
initialNumToRender={100}
maxToRenderPerBatch={50}
windowSize={10}
removeClippedSubviews={true}
getItemLayout={(_, index) => ({
length: emojiSize.itemHeight,
offset: emojiSize.itemHeight * Math.floor(index / emojiColumns),
index,
})}
/>
);

View File

@@ -29,6 +29,9 @@ import { useMarkAsRead } from '../../../stores/messageManagerHooks';
import { messageService } from '../../../services';
import { GroupInfoPanel } from './ChatScreen/GroupInfoPanel';
import { LongPressMenu, MenuPosition, GroupMessage } from './ChatScreen';
import { EmojiPanel } from './ChatScreen/EmojiPanel';
import { MORE_ACTIONS } from './ChatScreen/constants';
import * as ImagePicker from 'expo-image-picker';
const { width: screenWidth } = Dimensions.get('window');
const PANEL_WIDTH = 360;
@@ -69,6 +72,12 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
// 回复状态
const [replyingToMessage, setReplyingToMessage] = useState<GroupMessage | null>(null);
// 表情面板状态
const [showEmojiPanel, setShowEmojiPanel] = useState(false);
// 更多功能面板状态
const [showMorePanel, setShowMorePanel] = useState(false);
// 群信息面板动画
const slideAnim = useRef(new Animated.Value(PANEL_WIDTH)).current;
const fadeAnim = useRef(new Animated.Value(0)).current;
@@ -265,6 +274,94 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
console.error('[EmbeddedChat] 删除消息失败:', error);
}
};
// 处理插入表情
const handleInsertEmoji = (emoji: string) => {
setInputText(prev => prev + emoji);
};
// 处理插入自定义表情
const handleInsertSticker = async (stickerUrl: string) => {
try {
setSending(true);
const segments: MessageSegment[] = [{
type: 'image',
data: {
url: stickerUrl,
thumbnail_url: stickerUrl,
width: 200,
height: 200
}
}];
await messageManager.sendMessage(String(conversation.id), segments);
setShowEmojiPanel(false);
} catch (error) {
console.error('[EmbeddedChat] 发送表情失败:', error);
} finally {
setSending(false);
}
};
// 处理选择图片
const handlePickImage = async () => {
try {
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permissionResult.granted) {
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.8,
allowsMultipleSelection: true,
selectionLimit: 9,
});
if (!result.canceled && result.assets.length > 0) {
setSending(true);
for (const asset of result.assets) {
const segments: MessageSegment[] = [{
type: 'image',
data: {
url: asset.uri,
thumbnail_url: asset.uri,
width: asset.width || 800,
height: asset.height || 600
}
}];
await messageManager.sendMessage(String(conversation.id), segments);
}
setShowMorePanel(false);
}
} catch (error) {
console.error('[EmbeddedChat] 选择图片失败:', error);
} finally {
setSending(false);
}
};
// 处理更多功能项点击
const handleMoreAction = (actionId: string) => {
switch (actionId) {
case 'image':
handlePickImage();
break;
case 'camera':
// TODO: 实现相机拍摄
console.log('Camera');
break;
case 'file':
// TODO: 实现文件选择
console.log('File');
break;
case 'location':
// TODO: 实现位置分享
console.log('Location');
break;
}
};
// 格式化时间
const formatTime = (dateString: string) => {
@@ -520,10 +617,56 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
>
{/* 表情面板 */}
{showEmojiPanel && (
<View style={styles.emojiPanelContainer}>
<EmojiPanel
onInsertEmoji={handleInsertEmoji}
onInsertSticker={handleInsertSticker}
onClose={() => setShowEmojiPanel(false)}
/>
</View>
)}
{/* 更多功能面板 */}
{showMorePanel && (
<View style={styles.morePanelContainer}>
<View style={styles.morePanelHeader}>
<Text style={styles.morePanelTitle}></Text>
<TouchableOpacity onPress={() => setShowMorePanel(false)}>
<MaterialCommunityIcons name="close" size={24} color="#666" />
</TouchableOpacity>
</View>
<View style={styles.moreGrid}>
{MORE_ACTIONS.map(action => (
<TouchableOpacity
key={action.id}
style={styles.moreItem}
onPress={() => handleMoreAction(action.id)}
>
<View style={[styles.moreIconContainer, { backgroundColor: action.color + '15' }]}>
<MaterialCommunityIcons name={action.icon as any} size={28} color={action.color} />
</View>
<Text style={styles.moreItemText}>{action.name}</Text>
</TouchableOpacity>
))}
</View>
</View>
)}
<View style={styles.inputArea}>
<View style={styles.inputRow}>
<TouchableOpacity style={styles.iconButton}>
<MaterialCommunityIcons name="plus-circle-outline" size={28} color="#666" />
<TouchableOpacity
style={styles.iconButton}
onPress={() => {
setShowEmojiPanel(false);
setShowMorePanel(!showMorePanel);
}}
>
<MaterialCommunityIcons
name={showMorePanel ? "close-circle" : "plus-circle-outline"}
size={28}
color="#666"
/>
</TouchableOpacity>
<View style={styles.inputContainer}>
@@ -544,8 +687,8 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
/>
</View>
<TouchableOpacity style={styles.iconButton}>
<MaterialCommunityIcons name="emoticon-outline" size={28} color="#666" />
<TouchableOpacity style={styles.iconButton} onPress={() => { setShowMorePanel(false); setShowEmojiPanel(!showEmojiPanel); }}>
<MaterialCommunityIcons name={showEmojiPanel ? "close-circle" : "emoticon-outline"} size={28} color="#666" />
</TouchableOpacity>
<TouchableOpacity
@@ -864,4 +1007,48 @@ const styles = StyleSheet.create({
fontSize: 12,
marginTop: 4,
},
emojiPanelContainer: {
backgroundColor: '#F5F7FA',
borderTopWidth: 1,
borderTopColor: '#E8E8E8',
height: 350,
},
morePanelContainer: {
backgroundColor: '#FFF',
borderTopWidth: 1,
borderTopColor: '#E8E8E8',
padding: spacing.md,
},
morePanelHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.md,
},
morePanelTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333',
},
moreGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
moreItem: {
width: '25%',
alignItems: 'center',
marginBottom: spacing.md,
},
moreIconContainer: {
width: 56,
height: 56,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.xs,
},
moreItemText: {
fontSize: 12,
color: '#666',
},
});