diff --git a/MeAgent/navigation/Menu.js b/MeAgent/navigation/Menu.js
index 83297f54..87d6a8c4 100644
--- a/MeAgent/navigation/Menu.js
+++ b/MeAgent/navigation/Menu.js
@@ -166,6 +166,7 @@ function CustomDrawerContent({
{ title: "市场热点", navigateTo: "MarketDrawer", icon: "flame", gradient: ["#F59E0B", "#FBBF24"] },
{ title: "概念中心", navigateTo: "ConceptsDrawer", icon: "bulb", gradient: ["#06B6D4", "#22D3EE"] },
{ title: "我的自选", navigateTo: "WatchlistDrawer", icon: "star", gradient: ["#EC4899", "#F472B6"] },
+ { title: "社区论坛", navigateTo: "CommunityDrawer", icon: "chatbubbles", gradient: ["#10B981", "#34D399"] },
{ title: "个人中心", navigateTo: "ProfileDrawerNew", icon: "person", gradient: ["#8B5CF6", "#A78BFA"] },
];
return (
diff --git a/MeAgent/navigation/Screens.js b/MeAgent/navigation/Screens.js
index 811f8f89..b5ddcc77 100644
--- a/MeAgent/navigation/Screens.js
+++ b/MeAgent/navigation/Screens.js
@@ -50,6 +50,15 @@ import WatchlistScreen from "../src/screens/Watchlist/WatchlistScreen";
// 新股票详情页面
import { StockDetailScreen } from "../src/screens/StockDetail";
+// 社区页面
+import CommunityHome from "../src/screens/Community";
+import ChannelDetail from "../src/screens/Community/ChannelDetail";
+import ForumChannel from "../src/screens/Community/ForumChannel";
+import PostDetail from "../src/screens/Community/PostDetail";
+import CreatePost from "../src/screens/Community/CreatePost";
+import CreateChannel from "../src/screens/Community/CreateChannel";
+import MemberList from "../src/screens/Community/MemberList";
+
// 新个人中心页面
import { ProfileScreen as NewProfileScreen } from "../src/screens/Profile";
@@ -398,6 +407,68 @@ function WatchlistStack(props) {
);
}
+// 社区导航栈
+function CommunityStack(props) {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
// 新个人中心导航栈
function NewProfileStack(props) {
return (
@@ -705,6 +776,13 @@ function AppStack(props) {
headerShown: false,
}}
/>
+
{
const insets = useSafeAreaInsets();
+ // 自动跳转到主页
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ navigation.replace("App");
+ }, 1500); // 1.5秒后跳转
+
+ return () => clearTimeout(timer);
+ }, [navigation]);
+
return (
@@ -51,88 +56,64 @@ const Pro = ({ navigation }) => {
{/* 主内容 */}
-
- {/* 上部空间 */}
-
-
+
{/* Logo 区域 */}
-
-
+
+
+
+ {/* 品牌名称 */}
+
+
-
-
-
- {/* 品牌名称 */}
-
-
- 价值前沿
-
-
- VALUE FRONTIER
-
-
-
-
- {/* 特性描述 */}
-
+ 价值前沿
+
+ VALUE FRONTIER
+
+
+
+ {/* 特性描述 */}
+
+
+ 智能投资决策平台
+
+
- 智能投资决策平台{"\n"}
发现市场热点,把握投资机会
-
-
- {/* 底部按钮区域 */}
-
- {/* 开始使用按钮 */}
- navigation.navigate("App")}>
- {({ isPressed }) => (
-
-
- 开始使用
-
-
- )}
-
-
- {/* 版本信息 */}
-
-
- Version 1.0.0
-
-
+
+ {/* 加载指示器 */}
+
+
+
);
@@ -156,17 +137,6 @@ const styles = StyleSheet.create({
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 20,
},
- primaryButton: {
- height: 56,
- borderRadius: 16,
- alignItems: 'center',
- justifyContent: 'center',
- shadowColor: '#D4AF37',
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.4,
- shadowRadius: 12,
- elevation: 8,
- },
});
export default Pro;
diff --git a/MeAgent/src/hooks/useCommunitySocket.js b/MeAgent/src/hooks/useCommunitySocket.js
new file mode 100644
index 00000000..b9214555
--- /dev/null
+++ b/MeAgent/src/hooks/useCommunitySocket.js
@@ -0,0 +1,301 @@
+/**
+ * 社区 WebSocket Hook
+ * 管理社区实时通信连接
+ * 与 Web 端保持一致的事件名称和行为
+ */
+
+import { useEffect, useRef, useCallback, useState, useContext } from 'react';
+import { useDispatch } from 'react-redux';
+import { API_BASE_URL } from '../services/api';
+import AuthContext from '../contexts/AuthContext';
+import {
+ addMessage,
+ removeMessage,
+ updateMessageReactions,
+ addTypingUser,
+ removeTypingUser,
+ incrementUnreadCount,
+} from '../store/slices/communitySlice';
+
+// 服务器事件类型 - 与 Web 端保持一致(大写)
+export const SERVER_EVENTS = {
+ MESSAGE_CREATE: 'MESSAGE_CREATE',
+ MESSAGE_UPDATE: 'MESSAGE_UPDATE',
+ MESSAGE_DELETE: 'MESSAGE_DELETE',
+ REACTION_ADD: 'REACTION_ADD',
+ REACTION_REMOVE: 'REACTION_REMOVE',
+ TYPING_START: 'TYPING_START',
+ TYPING_STOP: 'TYPING_STOP',
+ MEMBER_JOIN: 'MEMBER_JOIN',
+ MEMBER_LEAVE: 'MEMBER_LEAVE',
+ MEMBER_ONLINE: 'MEMBER_ONLINE',
+ MEMBER_OFFLINE: 'MEMBER_OFFLINE',
+ CHANNEL_UPDATE: 'CHANNEL_UPDATE',
+};
+
+// 客户端事件类型 - 与 Web 端保持一致(大写)
+export const CLIENT_EVENTS = {
+ SUBSCRIBE_CHANNEL: 'SUBSCRIBE_CHANNEL',
+ UNSUBSCRIBE_CHANNEL: 'UNSUBSCRIBE_CHANNEL',
+ SEND_MESSAGE: 'SEND_MESSAGE',
+ START_TYPING: 'START_TYPING',
+ STOP_TYPING: 'STOP_TYPING',
+};
+
+/**
+ * 社区 WebSocket Hook
+ * @returns {object} WebSocket 操作方法
+ */
+export const useCommunitySocket = () => {
+ const dispatch = useDispatch();
+ // 安全获取 auth context,避免在 AuthProvider 外部使用时报错
+ const authContext = useContext(AuthContext);
+ const isLoggedIn = authContext?.isLoggedIn ?? false;
+ const socketRef = useRef(null);
+ const [isConnected, setIsConnected] = useState(false);
+ const [connectionError, setConnectionError] = useState(null);
+ const subscribedChannelsRef = useRef(new Set());
+ const typingTimeoutRef = useRef(null);
+ const eventHandlersRef = useRef(new Map());
+
+ // 初始化 Socket 连接 - 只有登录后才连接
+ useEffect(() => {
+ if (!isLoggedIn) {
+ // 未登录时断开连接
+ if (socketRef.current) {
+ socketRef.current.disconnect();
+ socketRef.current = null;
+ setIsConnected(false);
+ }
+ return;
+ }
+
+ // 动态导入 socket.io-client
+ const initSocket = async () => {
+ try {
+ const { io } = await import('socket.io-client');
+ const socketUrl = API_BASE_URL;
+
+ const socket = io(`${socketUrl}/community`, {
+ transports: ['websocket', 'polling'],
+ autoConnect: true,
+ reconnection: true,
+ reconnectionAttempts: 3,
+ reconnectionDelay: 2000,
+ reconnectionDelayMax: 10000,
+ timeout: 10000,
+ });
+
+ // 连接成功
+ socket.on('connect', () => {
+ console.log('[CommunitySocket] 连接成功');
+ setIsConnected(true);
+ setConnectionError(null);
+
+ // 重新订阅之前的频道
+ subscribedChannelsRef.current.forEach((channelId) => {
+ socket.emit(CLIENT_EVENTS.SUBSCRIBE_CHANNEL, { channelId });
+ });
+ });
+
+ // 连接断开
+ socket.on('disconnect', (reason) => {
+ console.log('[CommunitySocket] 连接断开:', reason);
+ setIsConnected(false);
+ });
+
+ // 连接错误 - 只是警告,不阻断页面
+ socket.on('connect_error', (error) => {
+ console.warn('[CommunitySocket] WebSocket 暂不可用,使用离线模式');
+ setConnectionError('WebSocket 暂不可用');
+ });
+
+ // 监听服务器事件
+ const serverEvents = [
+ SERVER_EVENTS.MESSAGE_CREATE,
+ SERVER_EVENTS.MESSAGE_UPDATE,
+ SERVER_EVENTS.MESSAGE_DELETE,
+ SERVER_EVENTS.REACTION_ADD,
+ SERVER_EVENTS.REACTION_REMOVE,
+ SERVER_EVENTS.TYPING_START,
+ SERVER_EVENTS.TYPING_STOP,
+ SERVER_EVENTS.MEMBER_JOIN,
+ SERVER_EVENTS.MEMBER_LEAVE,
+ SERVER_EVENTS.MEMBER_ONLINE,
+ SERVER_EVENTS.MEMBER_OFFLINE,
+ SERVER_EVENTS.CHANNEL_UPDATE,
+ ];
+
+ serverEvents.forEach((event) => {
+ socket.on(event, (data) => {
+ // 调用自定义事件处理器
+ const listeners = eventHandlersRef.current.get(event);
+ if (listeners) {
+ listeners.forEach((callback) => callback(data));
+ }
+
+ // 内置的 Redux dispatch 处理
+ switch (event) {
+ case SERVER_EVENTS.MESSAGE_CREATE:
+ dispatch(
+ addMessage({
+ channelId: data.channelId,
+ message: data.message,
+ })
+ );
+ break;
+ case SERVER_EVENTS.MESSAGE_DELETE:
+ dispatch(
+ removeMessage({
+ channelId: data.channelId,
+ messageId: data.messageId,
+ })
+ );
+ break;
+ case SERVER_EVENTS.REACTION_ADD:
+ case SERVER_EVENTS.REACTION_REMOVE:
+ dispatch(
+ updateMessageReactions({
+ channelId: data.channelId,
+ messageId: data.messageId,
+ reactions: data.reactions,
+ })
+ );
+ break;
+ case SERVER_EVENTS.TYPING_START:
+ dispatch(
+ addTypingUser({
+ channelId: data.channelId,
+ user: { userId: data.userId, username: data.userName || data.username },
+ })
+ );
+ break;
+ case SERVER_EVENTS.TYPING_STOP:
+ dispatch(
+ removeTypingUser({
+ channelId: data.channelId,
+ userId: data.userId,
+ })
+ );
+ break;
+ default:
+ break;
+ }
+ });
+ });
+
+ socketRef.current = socket;
+ } catch (error) {
+ console.warn('[CommunitySocket] Socket.IO 初始化失败,使用离线模式');
+ setConnectionError('初始化失败');
+ }
+ };
+
+ initSocket();
+
+ return () => {
+ if (socketRef.current) {
+ socketRef.current.disconnect();
+ socketRef.current = null;
+ }
+ if (typingTimeoutRef.current) {
+ clearTimeout(typingTimeoutRef.current);
+ }
+ };
+ }, [isLoggedIn, dispatch]);
+
+ // 订阅频道
+ const subscribe = useCallback((channelId) => {
+ if (!channelId) return;
+
+ subscribedChannelsRef.current.add(channelId);
+
+ if (socketRef.current?.connected) {
+ socketRef.current.emit(CLIENT_EVENTS.SUBSCRIBE_CHANNEL, { channelId });
+ console.log('[CommunitySocket] 已订阅频道:', channelId);
+ }
+ }, []);
+
+ // 取消订阅频道
+ const unsubscribe = useCallback((channelId) => {
+ if (!channelId) return;
+
+ subscribedChannelsRef.current.delete(channelId);
+
+ if (socketRef.current?.connected) {
+ socketRef.current.emit(CLIENT_EVENTS.UNSUBSCRIBE_CHANNEL, { channelId });
+ console.log('[CommunitySocket] 已取消订阅频道:', channelId);
+ }
+ }, []);
+
+ // 发送消息
+ const sendMessage = useCallback((channelId, content, options = {}) => {
+ if (!socketRef.current?.connected) {
+ return false;
+ }
+
+ socketRef.current.emit(CLIENT_EVENTS.SEND_MESSAGE, {
+ channelId,
+ content,
+ ...options,
+ });
+
+ return true;
+ }, []);
+
+ // 开始输入
+ const startTyping = useCallback((channelId) => {
+ if (!socketRef.current?.connected || !channelId) return;
+
+ if (typingTimeoutRef.current) {
+ clearTimeout(typingTimeoutRef.current);
+ }
+
+ socketRef.current.emit(CLIENT_EVENTS.START_TYPING, { channelId });
+
+ typingTimeoutRef.current = setTimeout(() => {
+ stopTyping(channelId);
+ }, 3000);
+ }, []);
+
+ // 停止输入
+ const stopTyping = useCallback((channelId) => {
+ if (!socketRef.current?.connected || !channelId) return;
+
+ if (typingTimeoutRef.current) {
+ clearTimeout(typingTimeoutRef.current);
+ typingTimeoutRef.current = null;
+ }
+
+ socketRef.current.emit(CLIENT_EVENTS.STOP_TYPING, { channelId });
+ }, []);
+
+ // 注册事件监听
+ const onMessage = useCallback((event, callback) => {
+ if (!eventHandlersRef.current.has(event)) {
+ eventHandlersRef.current.set(event, new Set());
+ }
+ eventHandlersRef.current.get(event).add(callback);
+ }, []);
+
+ // 移除事件监听
+ const offMessage = useCallback((event, callback) => {
+ if (eventHandlersRef.current.has(event)) {
+ eventHandlersRef.current.get(event).delete(callback);
+ }
+ }, []);
+
+ return {
+ isConnected,
+ connectionError,
+ subscribe,
+ unsubscribe,
+ sendMessage,
+ startTyping,
+ stopTyping,
+ onMessage,
+ offMessage,
+ };
+};
+
+export default useCommunitySocket;
diff --git a/MeAgent/src/screens/Community/ChannelDetail.js b/MeAgent/src/screens/Community/ChannelDetail.js
new file mode 100644
index 00000000..110e7c83
--- /dev/null
+++ b/MeAgent/src/screens/Community/ChannelDetail.js
@@ -0,0 +1,585 @@
+/**
+ * 频道详情页面
+ * 显示消息列表和输入框
+ */
+
+import React, { useEffect, useState, useCallback, useRef } from 'react';
+import {
+ FlatList,
+ KeyboardAvoidingView,
+ Platform,
+ StyleSheet,
+ Keyboard,
+ TextInput,
+ Image,
+ Dimensions,
+} from 'react-native';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Icon,
+ Pressable,
+ Spinner,
+ Center,
+ Avatar,
+} from 'native-base';
+import { Ionicons } from '@expo/vector-icons';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { useDispatch, useSelector } from 'react-redux';
+
+import {
+ fetchMessages,
+ sendMessage,
+ addMessage,
+} from '../../store/slices/communitySlice';
+import { useCommunitySocket } from '../../hooks/useCommunitySocket';
+
+// 消息分组:按日期
+const groupMessagesByDate = (messages) => {
+ const groups = [];
+ let currentDate = null;
+ let currentGroup = null;
+
+ messages.forEach((message) => {
+ const messageDate = new Date(message.createdAt).toDateString();
+
+ if (messageDate !== currentDate) {
+ currentDate = messageDate;
+ currentGroup = {
+ date: message.createdAt,
+ messages: [message],
+ };
+ groups.push(currentGroup);
+ } else {
+ currentGroup.messages.push(message);
+ }
+ });
+
+ return groups;
+};
+
+// 格式化日期分隔符
+const formatDateDivider = (dateStr) => {
+ const date = new Date(dateStr);
+ const today = new Date();
+ const yesterday = new Date(today);
+ yesterday.setDate(yesterday.getDate() - 1);
+
+ if (date.toDateString() === today.toDateString()) {
+ return '今天';
+ }
+ if (date.toDateString() === yesterday.toDateString()) {
+ return '昨天';
+ }
+
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ const year = date.getFullYear();
+
+ if (year === today.getFullYear()) {
+ return `${month}月${day}日`;
+ }
+ return `${year}年${month}月${day}日`;
+};
+
+// 格式化时间
+const formatTime = (dateStr) => {
+ const date = new Date(dateStr);
+ const hours = date.getHours().toString().padStart(2, '0');
+ const minutes = date.getMinutes().toString().padStart(2, '0');
+ return `${hours}:${minutes}`;
+};
+
+// 解析消息内容,提取文本和图片
+const parseMessageContent = (content) => {
+ if (!content) return { text: '', images: [] };
+
+ const images = [];
+ let text = content;
+
+ // 1. 匹配 Markdown 图片格式:  或 
+ const markdownImgRegex = /!\[([^\]]*)\]\((data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+|https?:\/\/[^\s)]+)\)/g;
+ let mdMatch;
+ while ((mdMatch = markdownImgRegex.exec(content)) !== null) {
+ if (!images.includes(mdMatch[2])) {
+ images.push(mdMatch[2]);
+ }
+ text = text.replace(mdMatch[0], '').trim();
+ }
+
+ // 2. 匹配直接的 base64 图片格式: data:image/xxx;base64,xxxxx
+ const base64Regex = /(data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+)/g;
+ const matches = text.match(base64Regex);
+
+ if (matches) {
+ matches.forEach((match) => {
+ if (!images.includes(match)) {
+ images.push(match);
+ }
+ text = text.replace(match, '').trim();
+ });
+ }
+
+ // 3. 匹配 [图片] 标记后跟 base64 的情况
+ const imgTagRegex = /\[图片\]\s*(data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+)/g;
+ let imgMatch;
+ while ((imgMatch = imgTagRegex.exec(content)) !== null) {
+ if (!images.includes(imgMatch[1])) {
+ images.push(imgMatch[1]);
+ }
+ text = text.replace(imgMatch[0], '').trim();
+ }
+
+ // 移除多余的 [图片] 标记
+ text = text.replace(/\[图片\]/g, '').trim();
+
+ return { text, images };
+};
+
+// 获取屏幕宽度用于图片尺寸计算
+const screenWidth = Dimensions.get('window').width;
+
+// 判断是否显示头像(同用户5分钟内的消息合并)
+const shouldShowAvatar = (message, index, messages) => {
+ if (index === 0) return true;
+
+ const prevMessage = messages[index - 1];
+ if (prevMessage.authorId !== message.authorId) return true;
+
+ const timeDiff = new Date(message.createdAt) - new Date(prevMessage.createdAt);
+ return timeDiff > 5 * 60 * 1000; // 5分钟
+};
+
+const ChannelDetail = ({ route, navigation }) => {
+ const { channel } = route.params;
+ const dispatch = useDispatch();
+ const insets = useSafeAreaInsets();
+ const flatListRef = useRef(null);
+
+ const communityState = useSelector((state) => state.community);
+ const messages = communityState?.messages || {};
+ const messagesHasMore = communityState?.messagesHasMore || {};
+ const loading = communityState?.loading || {};
+ const typingUsers = communityState?.typingUsers || {};
+
+ const channelMessages = messages[channel?.id] || [];
+ const hasMore = messagesHasMore[channel?.id] ?? true;
+ const channelTypingUsers = typingUsers[channel?.id] || [];
+
+ const [inputText, setInputText] = useState('');
+ const [isSending, setIsSending] = useState(false);
+
+ // WebSocket 连接
+ const {
+ isConnected,
+ subscribe,
+ unsubscribe,
+ startTyping,
+ stopTyping,
+ } = useCommunitySocket();
+
+ // 订阅频道 WebSocket
+ useEffect(() => {
+ if (isConnected && channel?.id) {
+ subscribe(channel.id);
+ }
+
+ return () => {
+ if (channel?.id) {
+ unsubscribe(channel.id);
+ stopTyping(channel.id);
+ }
+ };
+ }, [isConnected, channel?.id, subscribe, unsubscribe, stopTyping]);
+
+ // 监听新消息滚动到底部
+ useEffect(() => {
+ if (channelMessages.length > 0) {
+ setTimeout(() => {
+ flatListRef.current?.scrollToEnd({ animated: true });
+ }, 100);
+ }
+ }, [channelMessages.length]);
+
+ // 输入时发送输入状态
+ const handleTextChange = useCallback((text) => {
+ setInputText(text);
+ if (text.trim() && isConnected && channel?.id) {
+ startTyping(channel.id);
+ }
+ }, [isConnected, channel?.id, startTyping]);
+
+ // 设置导航标题
+ useEffect(() => {
+ navigation.setOptions({
+ headerShown: true,
+ headerTitle: channel?.name || '频道',
+ headerStyle: {
+ backgroundColor: '#0F172A',
+ elevation: 0,
+ shadowOpacity: 0,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(255, 255, 255, 0.1)',
+ },
+ headerTintColor: '#FFFFFF',
+ headerTitleStyle: {
+ fontWeight: '600',
+ fontSize: 16,
+ },
+ headerRight: () => (
+
+ navigation.navigate('MemberList', { channel })}>
+
+
+ {}}>
+
+
+
+ ),
+ });
+ }, [navigation, channel]);
+
+ // 加载消息
+ useEffect(() => {
+ if (channel?.id) {
+ dispatch(fetchMessages({ channelId: channel.id }));
+ }
+ }, [dispatch, channel?.id]);
+
+ // 加载更多消息
+ const handleLoadMore = useCallback(() => {
+ if (loading.messages || !hasMore || channelMessages.length === 0 || !channel?.id) return;
+
+ const oldestMessage = channelMessages[0];
+ dispatch(
+ fetchMessages({
+ channelId: channel.id,
+ options: { before: oldestMessage.createdAt },
+ })
+ );
+ }, [dispatch, channel?.id, channelMessages, loading.messages, hasMore]);
+
+ // 发送消息
+ const handleSend = useCallback(async () => {
+ const content = inputText.trim();
+ if (!content || isSending || !channel?.id) return;
+
+ setIsSending(true);
+ Keyboard.dismiss();
+
+ // 停止输入状态
+ stopTyping(channel.id);
+
+ try {
+ await dispatch(
+ sendMessage({
+ channelId: channel.id,
+ data: { content },
+ })
+ ).unwrap();
+
+ setInputText('');
+ // 滚动到底部
+ setTimeout(() => {
+ flatListRef.current?.scrollToEnd({ animated: true });
+ }, 100);
+ } catch (error) {
+ console.error('发送消息失败:', error);
+ } finally {
+ setIsSending(false);
+ }
+ }, [dispatch, channel?.id, inputText, isSending, stopTyping]);
+
+ // 渲染日期分隔符
+ const renderDateDivider = (date) => (
+
+
+
+ {formatDateDivider(date)}
+
+
+
+ );
+
+ // 渲染单条消息
+ const renderMessage = ({ item: message, index }) => {
+ const showAvatar = shouldShowAvatar(message, index, channelMessages);
+ const { text, images } = parseMessageContent(message.content);
+
+ return (
+
+ {/* 头像区域 */}
+
+ {showAvatar ? (
+
+ {message.authorName?.[0]?.toUpperCase() || '?'}
+
+ ) : (
+
+ {formatTime(message.createdAt)}
+
+ )}
+
+
+ {/* 消息内容 */}
+
+ {showAvatar ? (
+
+
+ {message.authorName || '匿名用户'}
+
+
+ {formatTime(message.createdAt)}
+
+
+ ) : null}
+
+ {/* 文本内容 */}
+ {text ? (
+
+ {text}
+
+ ) : null}
+
+ {/* Base64 图片 */}
+ {images.length > 0 ? (
+
+ {images.map((imgUri, imgIndex) => (
+ {
+ // TODO: 可以添加图片预览功能
+ }}
+ >
+
+
+ ))}
+
+ ) : null}
+
+ {/* 表情反应 */}
+ {message.reactions && Object.keys(message.reactions).length > 0 ? (
+
+ {Object.entries(message.reactions).map(([emoji, users]) => (
+
+
+ {emoji}
+
+ {users.length}
+
+
+
+ ))}
+
+ ) : null}
+
+
+ );
+ };
+
+ // 渲染消息列表
+ const renderMessageList = () => {
+ if (loading.messages && channelMessages.length === 0) {
+ return (
+
+
+
+ 加载消息...
+
+
+ );
+ }
+
+ if (channelMessages.length === 0) {
+ return (
+
+
+
+ 还没有消息
+
+
+ 成为第一个发言的人吧!
+
+
+ );
+ }
+
+ return (
+ item.id}
+ inverted={false}
+ onEndReached={handleLoadMore}
+ onEndReachedThreshold={0.1}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.messageList}
+ ListHeaderComponent={
+ hasMore && loading.messages ? (
+
+
+
+ ) : null
+ }
+ />
+ );
+ };
+
+ // 渲染输入中提示
+ const renderTypingIndicator = () => {
+ if (channelTypingUsers.length === 0) return null;
+
+ const names = channelTypingUsers.map((u) => u.username).join('、');
+ return (
+
+
+
+
+
+
+
+ {names} 正在输入...
+
+
+ );
+ };
+
+ // 渲染输入框
+ const renderInput = () => (
+
+ {renderTypingIndicator()}
+
+ {/* 附件按钮 */}
+
+
+
+
+ {/* 输入框 */}
+
+
+
+
+ {/* 发送按钮 */}
+
+ {isSending ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+
+ return (
+
+
+ {/* 频道话题 */}
+ {channel?.topic ? (
+
+
+
+
+ {channel.topic}
+
+
+
+ ) : null}
+
+ {/* 消息列表 */}
+ {renderMessageList()}
+
+ {/* 输入框 */}
+ {renderInput()}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#0F172A',
+ },
+ messageList: {
+ paddingVertical: 8,
+ },
+ textInput: {
+ flex: 1,
+ color: '#FFFFFF',
+ fontSize: 14,
+ maxHeight: 100,
+ minHeight: 36,
+ paddingVertical: Platform.OS === 'ios' ? 8 : 10,
+ paddingHorizontal: 0,
+ },
+});
+
+export default ChannelDetail;
diff --git a/MeAgent/src/screens/Community/ChannelList.js b/MeAgent/src/screens/Community/ChannelList.js
new file mode 100644
index 00000000..62a4c389
--- /dev/null
+++ b/MeAgent/src/screens/Community/ChannelList.js
@@ -0,0 +1,371 @@
+/**
+ * 频道列表页面
+ * Discord 风格的频道分类展示
+ */
+
+import React, { useEffect, useState, useCallback } from 'react';
+import {
+ SectionList,
+ RefreshControl,
+ StyleSheet,
+ Pressable as RNPressable,
+} from 'react-native';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Icon,
+ Input,
+ Pressable,
+ Spinner,
+ Center,
+} from 'native-base';
+import { Ionicons } from '@expo/vector-icons';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { fetchChannels, setCurrentChannel } from '../../store/slices/communitySlice';
+import { CHANNEL_TYPES } from '../../services/communityService';
+
+// 频道图标映射
+const CHANNEL_ICONS = {
+ [CHANNEL_TYPES.TEXT]: 'chatbubble',
+ [CHANNEL_TYPES.FORUM]: 'document-text',
+ [CHANNEL_TYPES.ANNOUNCEMENT]: 'megaphone',
+};
+
+// 分类图标映射
+const CATEGORY_ICONS = {
+ general: 'chatbubbles',
+ hot: 'flame',
+ announcement: 'megaphone',
+};
+
+const ChannelList = ({ navigation }) => {
+ const dispatch = useDispatch();
+ const communityState = useSelector((state) => state.community);
+ const categories = communityState?.categories || [];
+ const loading = communityState?.loading || {};
+
+ const [searchText, setSearchText] = useState('');
+ const [collapsedCategories, setCollapsedCategories] = useState(new Set());
+ const [refreshing, setRefreshing] = useState(false);
+
+ // 加载频道列表
+ useEffect(() => {
+ dispatch(fetchChannels());
+ }, [dispatch]);
+
+ // 下拉刷新
+ const handleRefresh = useCallback(async () => {
+ setRefreshing(true);
+ await dispatch(fetchChannels());
+ setRefreshing(false);
+ }, [dispatch]);
+
+ // 切换分类折叠状态
+ const toggleCategory = useCallback((categoryId) => {
+ setCollapsedCategories((prev) => {
+ const newSet = new Set(prev);
+ if (newSet.has(categoryId)) {
+ newSet.delete(categoryId);
+ } else {
+ newSet.add(categoryId);
+ }
+ return newSet;
+ });
+ }, []);
+
+ // 点击频道
+ const handleChannelPress = useCallback((channel) => {
+ dispatch(setCurrentChannel(channel));
+
+ // 根据频道类型导航到不同页面
+ if (channel.type === CHANNEL_TYPES.FORUM) {
+ navigation.navigate('ForumChannel', { channel });
+ } else {
+ navigation.navigate('ChannelDetail', { channel });
+ }
+ }, [dispatch, navigation]);
+
+ // 过滤频道(安全处理 undefined)
+ const filteredCategories = (categories || []).map((category) => ({
+ ...category,
+ data: collapsedCategories.has(category.id)
+ ? []
+ : (category.channels || []).filter((channel) =>
+ channel.name?.toLowerCase().includes(searchText.toLowerCase())
+ ),
+ }));
+
+ // 渲染搜索栏
+ const renderHeader = () => (
+
+
+ }
+ _focus={{
+ borderColor: 'primary.500',
+ bg: 'rgba(255, 255, 255, 0.08)',
+ }}
+ />
+
+ );
+
+ // 渲染分类标题
+ const renderSectionHeader = ({ section }) => {
+ if (!section) return null;
+
+ return (
+ section.id && toggleCategory(section.id)}>
+
+
+
+
+ {section.name || '未命名分类'}
+
+
+
+
+
+ );
+ };
+
+ // 渲染频道项
+ const renderChannelItem = ({ item: channel }) => {
+ if (!channel) return null;
+
+ const unreadCount = channel.unreadCount || 0;
+ const channelName = channel.name || '未命名频道';
+ const channelType = channel.type || CHANNEL_TYPES.TEXT;
+
+ return (
+ handleChannelPress(channel)}>
+ {({ isPressed }) => (
+
+ {/* 频道图标 */}
+ 0 ? 'rgba(124, 58, 237, 0.2)' : 'rgba(255, 255, 255, 0.05)'}
+ >
+ 0 ? '#A78BFA' : 'gray.500'}
+ />
+
+
+ {/* 频道信息 */}
+
+
+ 0 ? 'bold' : 'medium'}
+ color={unreadCount > 0 ? 'white' : 'gray.300'}
+ numberOfLines={1}
+ >
+ {channelName}
+
+ {channelType === CHANNEL_TYPES.ANNOUNCEMENT && (
+
+ )}
+
+ {channel.topic ? (
+
+ {channel.topic}
+
+ ) : null}
+
+
+ {/* 未读标记 */}
+ {unreadCount > 0 ? (
+
+
+ {unreadCount > 99 ? '99+' : String(unreadCount)}
+
+
+ ) : null}
+
+ {/* 成员数 */}
+ {!unreadCount && channel.subscriberCount ? (
+
+
+
+ {formatNumber(channel.subscriberCount)}
+
+
+ ) : null}
+
+ )}
+
+ );
+ };
+
+ // 渲染空状态
+ const renderEmpty = () => {
+ if (loading.channels) {
+ return (
+
+
+
+ 加载频道列表...
+
+
+ );
+ }
+
+ if (searchText) {
+ return (
+
+
+
+ 未找到匹配的频道
+
+
+ 尝试其他搜索词
+
+
+ );
+ }
+
+ return (
+
+
+
+ 暂无频道
+
+
+ );
+ };
+
+ // 渲染底部
+ const renderFooter = () => (
+
+ {
+ navigation.navigate('CreateChannel');
+ }}
+ >
+
+
+
+ 创建频道
+
+
+
+
+ );
+
+ return (
+
+ item?.id || `item-${index}`}
+ renderItem={renderChannelItem}
+ renderSectionHeader={renderSectionHeader}
+ ListHeaderComponent={renderHeader}
+ ListEmptyComponent={renderEmpty}
+ ListFooterComponent={renderFooter}
+ stickySectionHeadersEnabled={false}
+ refreshControl={
+
+ }
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.listContent}
+ />
+
+ );
+};
+
+// 格式化数字
+const formatNumber = (num) => {
+ if (num == null || isNaN(num)) return '0';
+ if (num >= 10000) {
+ return (num / 10000).toFixed(1) + 'w';
+ }
+ if (num >= 1000) {
+ return (num / 1000).toFixed(1) + 'k';
+ }
+ return String(num);
+};
+
+const styles = StyleSheet.create({
+ listContent: {
+ flexGrow: 1,
+ },
+});
+
+export default ChannelList;
diff --git a/MeAgent/src/screens/Community/CreateChannel.js b/MeAgent/src/screens/Community/CreateChannel.js
new file mode 100644
index 00000000..44e25adc
--- /dev/null
+++ b/MeAgent/src/screens/Community/CreateChannel.js
@@ -0,0 +1,378 @@
+/**
+ * 创建频道页面
+ */
+
+import React, { useState, useCallback, useEffect } from 'react';
+import {
+ ScrollView,
+ KeyboardAvoidingView,
+ Platform,
+ StyleSheet,
+ Keyboard,
+ Alert,
+ TextInput,
+} from 'react-native';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Icon,
+ Pressable,
+ Spinner,
+ Center,
+} from 'native-base';
+import { Ionicons } from '@expo/vector-icons';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { channelService, CHANNEL_TYPES } from '../../services/communityService';
+import { fetchChannels } from '../../store/slices/communitySlice';
+
+// 频道类型选项
+const CHANNEL_TYPE_OPTIONS = [
+ {
+ value: CHANNEL_TYPES.TEXT,
+ label: '文字频道',
+ icon: 'chatbubble',
+ description: '实时消息聊天',
+ },
+ {
+ value: CHANNEL_TYPES.FORUM,
+ label: '论坛频道',
+ icon: 'document-text',
+ description: '发帖讨论,支持长文',
+ },
+];
+
+const CreateChannel = ({ navigation }) => {
+ const dispatch = useDispatch();
+ const insets = useSafeAreaInsets();
+
+ // 从 Redux 获取真实的 categories 列表
+ const communityState = useSelector((state) => state.community);
+ const categories = communityState?.categories || [];
+ const loadingCategories = communityState?.loading?.channels || false;
+
+ const [name, setName] = useState('');
+ const [topic, setTopic] = useState('');
+ const [channelType, setChannelType] = useState(CHANNEL_TYPES.TEXT);
+ const [category, setCategory] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // 加载 categories(如果还没有加载)
+ useEffect(() => {
+ if (categories.length === 0) {
+ dispatch(fetchChannels());
+ } else if (!category && categories.length > 0) {
+ // 设置默认选中第一个分类
+ setCategory(categories[0].id);
+ }
+ }, [dispatch, categories, category]);
+
+ const NAME_MAX = 50;
+ const TOPIC_MAX = 200;
+
+ // 验证表单
+ const validateForm = useCallback(() => {
+ if (!name.trim()) {
+ Alert.alert('提示', '请输入频道名称');
+ return false;
+ }
+ if (name.length > NAME_MAX) {
+ Alert.alert('提示', `频道名称不能超过${NAME_MAX}个字符`);
+ return false;
+ }
+ if (!category) {
+ Alert.alert('提示', '请选择所属分类');
+ return false;
+ }
+ if (topic.length > TOPIC_MAX) {
+ Alert.alert('提示', `频道话题不能超过${TOPIC_MAX}个字符`);
+ return false;
+ }
+ return true;
+ }, [name, topic, category]);
+
+ // 创建频道
+ const handleSubmit = useCallback(async () => {
+ if (!validateForm() || isSubmitting) return;
+
+ setIsSubmitting(true);
+ Keyboard.dismiss();
+
+ try {
+ const newChannel = await channelService.createChannel({
+ name: name.trim(),
+ type: channelType,
+ topic: topic.trim() || undefined,
+ categoryId: category,
+ });
+
+ // 刷新频道列表
+ dispatch(fetchChannels());
+
+ Alert.alert('创建成功', `已创建频道 #${newChannel.name || name}`, [
+ {
+ text: '进入频道',
+ onPress: () => {
+ navigation.goBack();
+ // 如果需要直接进入新频道,可以导航到对应页面
+ if (newChannel) {
+ if (channelType === CHANNEL_TYPES.FORUM) {
+ navigation.navigate('ForumChannel', { channel: newChannel });
+ } else {
+ navigation.navigate('ChannelDetail', { channel: newChannel });
+ }
+ }
+ },
+ },
+ {
+ text: '返回列表',
+ onPress: () => navigation.goBack(),
+ },
+ ]);
+ } catch (error) {
+ Alert.alert('创建失败', error.message || '请稍后重试');
+ } finally {
+ setIsSubmitting(false);
+ }
+ }, [dispatch, name, topic, channelType, category, validateForm, isSubmitting, navigation]);
+
+ // 设置导航
+ React.useEffect(() => {
+ navigation.setOptions({
+ headerShown: true,
+ headerTitle: '创建频道',
+ headerStyle: {
+ backgroundColor: '#0F172A',
+ elevation: 0,
+ shadowOpacity: 0,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(255, 255, 255, 0.1)',
+ },
+ headerTintColor: '#FFFFFF',
+ headerTitleStyle: {
+ fontWeight: '600',
+ fontSize: 16,
+ },
+ headerRight: () => (
+
+ {isSubmitting ? (
+
+ ) : (
+
+ 创建
+
+ )}
+
+ ),
+ });
+ }, [navigation, handleSubmit, isSubmitting, name]);
+
+ return (
+
+
+ {/* 频道名称 */}
+
+
+
+ 频道名称 *
+
+ NAME_MAX ? 'red.500' : 'gray.500'}>
+ {name.length}/{NAME_MAX}
+
+
+
+
+
+
+
+ {/* 频道类型 */}
+
+
+ 频道类型
+
+
+ {CHANNEL_TYPE_OPTIONS.map((option) => (
+ setChannelType(option.value)}
+ >
+
+
+
+
+ {option.label}
+
+
+ {option.description}
+
+
+
+
+ ))}
+
+
+
+ {/* 所属分类 */}
+
+
+ 所属分类
+
+ {loadingCategories ? (
+
+
+ 加载分类...
+
+ ) : categories.length === 0 ? (
+ 暂无可用分类
+ ) : (
+
+ {categories.map((cat) => (
+ setCategory(cat.id)}
+ mb={2}
+ >
+
+
+ {cat.name || cat.id}
+
+
+
+ ))}
+
+ )}
+
+
+ {/* 频道话题 */}
+
+
+
+ 频道话题(可选)
+
+ TOPIC_MAX ? 'red.500' : 'gray.500'}>
+ {topic.length}/{TOPIC_MAX}
+
+
+
+
+
+
+
+ {/* 提示 */}
+
+
+
+
+
+ 创建须知
+
+
+ {`• 频道名称应与讨论主题相关\n• 创建后您将成为频道管理员\n• 请遵守社区规范,文明交流`}
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#0F172A',
+ },
+ content: {
+ flexGrow: 1,
+ },
+ textInput: {
+ color: '#FFFFFF',
+ fontSize: 14,
+ minHeight: 40,
+ paddingVertical: Platform.OS === 'ios' ? 0 : 12,
+ },
+ textArea: {
+ minHeight: 80,
+ paddingTop: Platform.OS === 'ios' ? 0 : 12,
+ },
+});
+
+export default CreateChannel;
diff --git a/MeAgent/src/screens/Community/CreatePost.js b/MeAgent/src/screens/Community/CreatePost.js
new file mode 100644
index 00000000..e4f4db30
--- /dev/null
+++ b/MeAgent/src/screens/Community/CreatePost.js
@@ -0,0 +1,396 @@
+/**
+ * 发帖页面
+ * 创建新的论坛帖子
+ */
+
+import React, { useState, useCallback } from 'react';
+import {
+ ScrollView,
+ KeyboardAvoidingView,
+ Platform,
+ StyleSheet,
+ Keyboard,
+ Alert,
+} from 'react-native';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Icon,
+ Pressable,
+ Input,
+ TextArea,
+ Spinner,
+} from 'native-base';
+import { LinearGradient } from 'expo-linear-gradient';
+import { Ionicons } from '@expo/vector-icons';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { useDispatch } from 'react-redux';
+
+import { createPost } from '../../store/slices/communitySlice';
+import { gradients } from '../../theme';
+
+// 预设标签
+const PRESET_TAGS = [
+ '技术分析', '基本面', '短线', '中长线', '新手',
+ '价值投资', '趋势', '消息面', '情绪', '复盘',
+];
+
+const CreatePost = ({ route, navigation }) => {
+ const { channel } = route.params;
+ const dispatch = useDispatch();
+ const insets = useSafeAreaInsets();
+
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [tags, setTags] = useState([]);
+ const [customTag, setCustomTag] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const TITLE_MAX = 100;
+ const CONTENT_MAX = 5000;
+ const TAGS_MAX = 5;
+
+ // 添加标签
+ const handleAddTag = useCallback((tag) => {
+ if (tags.length >= TAGS_MAX) {
+ Alert.alert('提示', `最多添加${TAGS_MAX}个标签`);
+ return;
+ }
+ if (tags.includes(tag)) {
+ return;
+ }
+ setTags([...tags, tag]);
+ }, [tags]);
+
+ // 移除标签
+ const handleRemoveTag = useCallback((tag) => {
+ setTags(tags.filter((t) => t !== tag));
+ }, [tags]);
+
+ // 添加自定义标签
+ const handleAddCustomTag = useCallback(() => {
+ const tag = customTag.trim();
+ if (!tag) return;
+ if (tag.length > 20) {
+ Alert.alert('提示', '标签长度不能超过20个字符');
+ return;
+ }
+ handleAddTag(tag);
+ setCustomTag('');
+ }, [customTag, handleAddTag]);
+
+ // 验证表单
+ const validateForm = useCallback(() => {
+ if (!title.trim()) {
+ Alert.alert('提示', '请输入标题');
+ return false;
+ }
+ if (title.length > TITLE_MAX) {
+ Alert.alert('提示', `标题不能超过${TITLE_MAX}个字符`);
+ return false;
+ }
+ if (!content.trim()) {
+ Alert.alert('提示', '请输入内容');
+ return false;
+ }
+ if (content.length > CONTENT_MAX) {
+ Alert.alert('提示', `内容不能超过${CONTENT_MAX}个字符`);
+ return false;
+ }
+ return true;
+ }, [title, content]);
+
+ // 提交帖子
+ const handleSubmit = useCallback(async () => {
+ if (!validateForm() || isSubmitting) return;
+
+ setIsSubmitting(true);
+ Keyboard.dismiss();
+
+ try {
+ await dispatch(
+ createPost({
+ channelId: channel.id,
+ data: {
+ title: title.trim(),
+ content: content.trim(),
+ tags,
+ },
+ })
+ ).unwrap();
+
+ Alert.alert('成功', '帖子发布成功', [
+ {
+ text: '确定',
+ onPress: () => navigation.goBack(),
+ },
+ ]);
+ } catch (error) {
+ Alert.alert('发布失败', error.message || '请稍后重试');
+ } finally {
+ setIsSubmitting(false);
+ }
+ }, [dispatch, channel.id, title, content, tags, validateForm, isSubmitting, navigation]);
+
+ // 设置导航
+ React.useEffect(() => {
+ navigation.setOptions({
+ headerShown: true,
+ headerTitle: '发布帖子',
+ headerStyle: {
+ backgroundColor: '#0F172A',
+ elevation: 0,
+ shadowOpacity: 0,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(255, 255, 255, 0.1)',
+ },
+ headerTintColor: '#FFFFFF',
+ headerTitleStyle: {
+ fontWeight: '600',
+ fontSize: 16,
+ },
+ headerRight: () => (
+
+ {isSubmitting ? (
+
+ ) : (
+
+ 发布
+
+ )}
+
+ ),
+ });
+ }, [navigation, handleSubmit, isSubmitting, title, content]);
+
+ return (
+
+
+ {/* 频道信息 */}
+
+
+
+
+ 发布到: {channel.name}
+
+
+
+
+ {/* 标题输入 */}
+
+
+
+ 标题
+
+ TITLE_MAX ? 'red.500' : 'gray.500'}>
+ {title.length}/{TITLE_MAX}
+
+
+
+
+
+ {/* 内容输入 */}
+
+
+
+ 内容
+
+ CONTENT_MAX ? 'red.500' : 'gray.500'}>
+ {content.length}/{CONTENT_MAX}
+
+
+
+
+
+ {/* 标签选择 */}
+
+
+
+ 标签
+
+
+ {tags.length}/{TAGS_MAX}
+
+
+
+ {/* 已选标签 */}
+ {tags.length > 0 && (
+
+ {tags.map((tag) => (
+ handleRemoveTag(tag)}>
+
+
+ #{tag}
+
+
+
+
+ ))}
+
+ )}
+
+ {/* 自定义标签输入 */}
+
+
+
+
+ 添加
+
+
+
+
+ {/* 预设标签 */}
+
+ {PRESET_TAGS.map((tag) => (
+ handleAddTag(tag)}
+ disabled={tags.includes(tag)}
+ mb={2}
+ >
+
+
+ #{tag}
+
+
+
+ ))}
+
+
+
+ {/* 提示 */}
+
+
+
+
+
+ 发帖须知
+
+
+ • 请遵守社区规范,文明发言{'\n'}
+ • 禁止发布违法、低俗、广告内容{'\n'}
+ • 投资有风险,观点仅供参考
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#0F172A',
+ },
+ content: {
+ flexGrow: 1,
+ },
+});
+
+export default CreatePost;
diff --git a/MeAgent/src/screens/Community/ForumChannel.js b/MeAgent/src/screens/Community/ForumChannel.js
new file mode 100644
index 00000000..eef9c722
--- /dev/null
+++ b/MeAgent/src/screens/Community/ForumChannel.js
@@ -0,0 +1,421 @@
+/**
+ * 论坛频道页面
+ * 显示帖子列表,支持发帖
+ */
+
+import React, { useEffect, useState, useCallback } from 'react';
+import {
+ FlatList,
+ RefreshControl,
+ StyleSheet,
+} from 'react-native';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Icon,
+ Pressable,
+ Spinner,
+ Center,
+ Avatar,
+ Fab,
+ Menu,
+} from 'native-base';
+import { LinearGradient } from 'expo-linear-gradient';
+import { Ionicons } from '@expo/vector-icons';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { fetchPosts } from '../../store/slices/communitySlice';
+import { gradients } from '../../theme';
+
+// 排序选项
+const SORT_OPTIONS = [
+ { key: 'latest', label: '最新发布', icon: 'time-outline' },
+ { key: 'hot', label: '最热门', icon: 'flame-outline' },
+ { key: 'most_replies', label: '最多回复', icon: 'chatbubbles-outline' },
+];
+
+// 格式化相对时间
+const formatRelativeTime = (dateStr) => {
+ const date = new Date(dateStr);
+ const now = new Date();
+ const diff = now - date;
+
+ const minutes = Math.floor(diff / 60000);
+ const hours = Math.floor(diff / 3600000);
+ const days = Math.floor(diff / 86400000);
+
+ if (minutes < 1) return '刚刚';
+ if (minutes < 60) return `${minutes}分钟前`;
+ if (hours < 24) return `${hours}小时前`;
+ if (days < 30) return `${days}天前`;
+
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ return `${month}月${day}日`;
+};
+
+// 格式化数字
+const formatNumber = (num) => {
+ if (num >= 10000) return (num / 10000).toFixed(1) + 'w';
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'k';
+ return num?.toString() || '0';
+};
+
+const ForumChannel = ({ route, navigation }) => {
+ const { channel } = route.params;
+ const dispatch = useDispatch();
+ const insets = useSafeAreaInsets();
+
+ const { posts, postsHasMore, loading } = useSelector((state) => state.community);
+ const channelPosts = posts[channel.id] || [];
+ const hasMore = postsHasMore[channel.id] ?? true;
+
+ const [sortBy, setSortBy] = useState('latest');
+ const [refreshing, setRefreshing] = useState(false);
+ const [page, setPage] = useState(1);
+
+ // 设置导航标题
+ useEffect(() => {
+ navigation.setOptions({
+ headerShown: true,
+ headerTitle: channel.name,
+ headerStyle: {
+ backgroundColor: '#0F172A',
+ elevation: 0,
+ shadowOpacity: 0,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(255, 255, 255, 0.1)',
+ },
+ headerTintColor: '#FFFFFF',
+ headerTitleStyle: {
+ fontWeight: '600',
+ fontSize: 16,
+ },
+ });
+ }, [navigation, channel]);
+
+ // 加载帖子
+ useEffect(() => {
+ dispatch(fetchPosts({ channelId: channel.id, options: { sortBy, page: 1 } }));
+ setPage(1);
+ }, [dispatch, channel.id, sortBy]);
+
+ // 下拉刷新
+ const handleRefresh = useCallback(async () => {
+ setRefreshing(true);
+ await dispatch(fetchPosts({ channelId: channel.id, options: { sortBy, page: 1 } }));
+ setPage(1);
+ setRefreshing(false);
+ }, [dispatch, channel.id, sortBy]);
+
+ // 加载更多
+ const handleLoadMore = useCallback(() => {
+ if (loading.posts || !hasMore) return;
+
+ const nextPage = page + 1;
+ dispatch(fetchPosts({ channelId: channel.id, options: { sortBy, page: nextPage } }));
+ setPage(nextPage);
+ }, [dispatch, channel.id, sortBy, page, loading.posts, hasMore]);
+
+ // 点击帖子
+ const handlePostPress = useCallback((post) => {
+ navigation.navigate('PostDetail', { post, channel });
+ }, [navigation, channel]);
+
+ // 发帖
+ const handleCreatePost = useCallback(() => {
+ navigation.navigate('CreatePost', { channel });
+ }, [navigation, channel]);
+
+ // 渲染排序栏
+ const renderSortBar = () => (
+
+
+ {SORT_OPTIONS.map((option) => (
+ setSortBy(option.key)}
+ >
+
+
+
+ {option.label}
+
+
+
+ ))}
+
+
+
+ {channelPosts.length} 篇帖子
+
+
+ );
+
+ // 渲染帖子卡片
+ const renderPostCard = ({ item: post }) => (
+ handlePostPress(post)} mx={4} mb={3}>
+ {({ isPressed }) => (
+
+ {/* 作者信息 */}
+
+
+ {post.authorName?.[0]?.toUpperCase() || '?'}
+
+
+
+ {post.authorName || '匿名用户'}
+
+
+ {formatRelativeTime(post.createdAt)}
+
+
+ {post.isPinned && (
+
+
+
+
+ 置顶
+
+
+
+ )}
+
+
+ {/* 标题 */}
+
+ {post.title}
+
+
+ {/* 内容预览 */}
+ {post.content ? (
+
+ {post.content
+ .replace(/!\[[^\]]*\]\([^)]+\)/g, '[图片]') // 移除 Markdown 图片
+ .replace(/<[^>]*>/g, '') // 移除 HTML 标签
+ .replace(/data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+/g, '[图片]') // 移除 base64 图片
+ .slice(0, 150)}
+
+ ) : null}
+
+ {/* 标签 */}
+ {post.tags && post.tags.length > 0 && (
+
+ {post.tags.slice(0, 3).map((tag, index) => (
+
+
+ #{tag}
+
+
+ ))}
+ {post.tags.length > 3 && (
+
+ +{post.tags.length - 3}
+
+ )}
+
+ )}
+
+ {/* 统计数据 */}
+
+
+
+
+ {formatNumber(post.likeCount)}
+
+
+
+
+
+ {formatNumber(post.replyCount)}
+
+
+
+
+
+ {formatNumber(post.viewCount)}
+
+
+
+
+ )}
+
+ );
+
+ // 渲染空状态
+ const renderEmpty = () => {
+ if (loading.posts) {
+ return (
+
+
+
+ 加载帖子列表...
+
+
+ );
+ }
+
+ return (
+
+
+
+ 还没有帖子
+
+
+ 成为第一个发帖的人吧!
+
+
+
+
+
+ 发布帖子
+
+
+
+
+ );
+ };
+
+ // 渲染加载更多
+ const renderFooter = () => {
+ if (!hasMore || channelPosts.length === 0) return null;
+
+ if (loading.posts) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+ };
+
+ return (
+
+ {/* 频道话题 */}
+ {channel.topic && (
+
+
+
+
+ {channel.topic}
+
+
+
+ )}
+
+ {/* 排序栏 */}
+ {renderSortBar()}
+
+ {/* 帖子列表 */}
+ item.id}
+ ListEmptyComponent={renderEmpty}
+ ListFooterComponent={renderFooter}
+ onEndReached={handleLoadMore}
+ onEndReachedThreshold={0.2}
+ refreshControl={
+
+ }
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={[
+ styles.listContent,
+ { paddingBottom: insets.bottom + 80 },
+ ]}
+ />
+
+ {/* 发帖按钮 */}
+ }
+ onPress={handleCreatePost}
+ position="absolute"
+ bottom={insets.bottom + 16}
+ right={4}
+ bg="primary.500"
+ _pressed={{ bg: 'primary.600' }}
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ listContent: {
+ flexGrow: 1,
+ },
+ createButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingVertical: 12,
+ borderRadius: 24,
+ },
+});
+
+export default ForumChannel;
diff --git a/MeAgent/src/screens/Community/MemberList.js b/MeAgent/src/screens/Community/MemberList.js
new file mode 100644
index 00000000..8397e153
--- /dev/null
+++ b/MeAgent/src/screens/Community/MemberList.js
@@ -0,0 +1,299 @@
+/**
+ * 成员列表页面
+ * 显示频道成员,区分在线/离线
+ */
+
+import React, { useEffect, useState, useCallback } from 'react';
+import {
+ SectionList,
+ RefreshControl,
+ StyleSheet,
+} from 'react-native';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Icon,
+ Pressable,
+ Spinner,
+ Center,
+ Avatar,
+ Input,
+} from 'native-base';
+import { Ionicons } from '@expo/vector-icons';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { fetchMembers } from '../../store/slices/communitySlice';
+
+// 角色配置
+const ROLE_CONFIG = {
+ owner: { label: '群主', color: '#D4AF37', icon: 'star' },
+ admin: { label: '管理员', color: '#7C3AED', icon: 'shield' },
+ moderator: { label: '版主', color: '#10B981', icon: 'ribbon' },
+};
+
+const MemberList = ({ route, navigation }) => {
+ const { channel } = route.params;
+ const dispatch = useDispatch();
+ const insets = useSafeAreaInsets();
+
+ const { members, loading } = useSelector((state) => state.community);
+ const channelMembers = members[channel.id] || [];
+
+ const [searchText, setSearchText] = useState('');
+ const [refreshing, setRefreshing] = useState(false);
+
+ // 设置导航
+ useEffect(() => {
+ navigation.setOptions({
+ headerShown: true,
+ headerTitle: '成员列表',
+ headerStyle: {
+ backgroundColor: '#0F172A',
+ elevation: 0,
+ shadowOpacity: 0,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(255, 255, 255, 0.1)',
+ },
+ headerTintColor: '#FFFFFF',
+ headerTitleStyle: {
+ fontWeight: '600',
+ fontSize: 16,
+ },
+ });
+ }, [navigation]);
+
+ // 加载成员
+ useEffect(() => {
+ dispatch(fetchMembers({ channelId: channel.id }));
+ }, [dispatch, channel.id]);
+
+ // 下拉刷新
+ const handleRefresh = useCallback(async () => {
+ setRefreshing(true);
+ await dispatch(fetchMembers({ channelId: channel.id }));
+ setRefreshing(false);
+ }, [dispatch, channel.id]);
+
+ // 过滤和分组成员
+ const filteredMembers = channelMembers.filter((member) =>
+ member.username?.toLowerCase().includes(searchText.toLowerCase())
+ );
+
+ const onlineMembers = filteredMembers.filter((m) => m.isOnline);
+ const offlineMembers = filteredMembers.filter((m) => !m.isOnline);
+
+ const sections = [
+ { title: '在线', data: onlineMembers, count: onlineMembers.length },
+ { title: '离线', data: offlineMembers, count: offlineMembers.length },
+ ].filter((section) => section.data.length > 0);
+
+ // 渲染搜索栏
+ const renderHeader = () => (
+
+
+ }
+ _focus={{
+ borderColor: 'primary.500',
+ bg: 'rgba(255, 255, 255, 0.08)',
+ }}
+ />
+
+ 共 {channelMembers.length} 位成员 · {onlineMembers.length} 人在线
+
+
+ );
+
+ // 渲染分组标题
+ const renderSectionHeader = ({ section }) => (
+
+
+
+ {section.title} — {section.count}
+
+
+ );
+
+ // 渲染成员项
+ const renderMemberItem = ({ item: member }) => {
+ const role = ROLE_CONFIG[member.role];
+
+ return (
+ {}}>
+ {({ isPressed }) => (
+
+ {/* 头像 */}
+
+
+ {member.username?.[0]?.toUpperCase() || '?'}
+
+ {/* 在线状态 */}
+
+
+
+ {/* 用户信息 */}
+
+
+
+ {member.username || '匿名用户'}
+
+ {role && (
+
+
+
+
+ {role.label}
+
+
+
+ )}
+
+ {member.bio && (
+
+ {member.bio}
+
+ )}
+
+
+ {/* 操作按钮 */}
+
+
+
+
+ )}
+
+ );
+ };
+
+ // 渲染空状态
+ const renderEmpty = () => {
+ if (loading.members) {
+ return (
+
+
+
+ 加载成员列表...
+
+
+ );
+ }
+
+ if (searchText) {
+ return (
+
+
+
+ 未找到匹配的成员
+
+
+ );
+ }
+
+ return (
+
+
+
+ 暂无成员
+
+
+ );
+ };
+
+ return (
+
+ item.userId}
+ renderItem={renderMemberItem}
+ renderSectionHeader={renderSectionHeader}
+ ListHeaderComponent={renderHeader}
+ ListEmptyComponent={renderEmpty}
+ stickySectionHeadersEnabled={false}
+ refreshControl={
+
+ }
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={[
+ styles.listContent,
+ { paddingBottom: insets.bottom + 20 },
+ ]}
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ listContent: {
+ flexGrow: 1,
+ },
+});
+
+export default MemberList;
diff --git a/MeAgent/src/screens/Community/Notifications.js b/MeAgent/src/screens/Community/Notifications.js
new file mode 100644
index 00000000..cd3390e2
--- /dev/null
+++ b/MeAgent/src/screens/Community/Notifications.js
@@ -0,0 +1,337 @@
+/**
+ * 通知页面
+ * 显示社区通知列表
+ */
+
+import React, { useEffect, useState, useCallback } from 'react';
+import {
+ FlatList,
+ RefreshControl,
+ StyleSheet,
+} from 'react-native';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Icon,
+ Pressable,
+ Spinner,
+ Center,
+ Avatar,
+} from 'native-base';
+import { Ionicons } from '@expo/vector-icons';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { useDispatch, useSelector } from 'react-redux';
+
+import {
+ fetchNotifications,
+ markNotificationRead,
+ markAllNotificationsRead,
+} from '../../store/slices/communitySlice';
+
+// 通知类型配置
+const NOTIFICATION_CONFIG = {
+ reply: {
+ icon: 'chatbubble',
+ color: '#7C3AED',
+ label: '回复了你的帖子',
+ },
+ mention: {
+ icon: 'at',
+ color: '#10B981',
+ label: '提到了你',
+ },
+ like: {
+ icon: 'heart',
+ color: '#F43F5E',
+ label: '赞了你的内容',
+ },
+ follow: {
+ icon: 'person-add',
+ color: '#3B82F6',
+ label: '关注了你',
+ },
+ system: {
+ icon: 'megaphone',
+ color: '#D4AF37',
+ label: '系统通知',
+ },
+};
+
+// 格式化相对时间
+const formatRelativeTime = (dateStr) => {
+ const date = new Date(dateStr);
+ const now = new Date();
+ const diff = now - date;
+
+ const minutes = Math.floor(diff / 60000);
+ const hours = Math.floor(diff / 3600000);
+ const days = Math.floor(diff / 86400000);
+
+ if (minutes < 1) return '刚刚';
+ if (minutes < 60) return `${minutes}分钟前`;
+ if (hours < 24) return `${hours}小时前`;
+ if (days < 7) return `${days}天前`;
+
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ return `${month}月${day}日`;
+};
+
+const Notifications = ({ navigation }) => {
+ const dispatch = useDispatch();
+ const insets = useSafeAreaInsets();
+
+ const communityState = useSelector((state) => state.community);
+ const notifications = communityState?.notifications || [];
+ const notificationsHasMore = communityState?.notificationsHasMore ?? true;
+ const loading = communityState?.loading || {};
+ const unreadCount = communityState?.unreadCount || 0;
+
+ const [refreshing, setRefreshing] = useState(false);
+ const [page, setPage] = useState(1);
+
+ // 加载通知
+ useEffect(() => {
+ dispatch(fetchNotifications({ page: 1 }));
+ setPage(1);
+ }, [dispatch]);
+
+ // 下拉刷新
+ const handleRefresh = useCallback(async () => {
+ setRefreshing(true);
+ await dispatch(fetchNotifications({ page: 1 }));
+ setPage(1);
+ setRefreshing(false);
+ }, [dispatch]);
+
+ // 加载更多
+ const handleLoadMore = useCallback(() => {
+ if (loading.notifications || !notificationsHasMore) return;
+
+ const nextPage = page + 1;
+ dispatch(fetchNotifications({ page: nextPage }));
+ setPage(nextPage);
+ }, [dispatch, page, loading.notifications, notificationsHasMore]);
+
+ // 标记已读
+ const handleMarkRead = useCallback((notification) => {
+ if (!notification.isRead) {
+ dispatch(markNotificationRead(notification.id));
+ }
+ // 根据通知类型跳转
+ if (notification.targetType === 'post' && notification.targetId) {
+ navigation.navigate('PostDetail', {
+ post: { id: notification.targetId },
+ channel: {},
+ });
+ }
+ }, [dispatch, navigation]);
+
+ // 标记全部已读
+ const handleMarkAllRead = useCallback(() => {
+ dispatch(markAllNotificationsRead());
+ }, [dispatch]);
+
+ // 渲染头部
+ const renderHeader = () => (
+
+
+
+ 通知
+
+ {unreadCount > 0 ? (
+
+
+ {unreadCount > 99 ? '99+' : String(unreadCount)}
+
+
+ ) : null}
+
+ {unreadCount > 0 ? (
+
+
+ 全部已读
+
+
+ ) : null}
+
+ );
+
+ // 渲染通知项
+ const renderNotificationItem = ({ item: notification }) => {
+ if (!notification) return null;
+
+ const config = NOTIFICATION_CONFIG[notification.type] || NOTIFICATION_CONFIG.system;
+
+ return (
+ handleMarkRead(notification)}>
+ {({ isPressed }) => (
+
+ {/* 图标/头像 */}
+
+ {notification.senderAvatar ? (
+
+ {notification.senderName?.[0]?.toUpperCase() || '?'}
+
+ ) : (
+
+
+
+ )}
+ {/* 未读标记 */}
+ {!notification.isRead && (
+
+ )}
+
+
+ {/* 内容 */}
+
+
+ {notification.senderName && (
+
+ {notification.senderName}
+
+ )}
+
+ {config.label}
+
+
+
+ {notification.content && (
+
+ {notification.content}
+
+ )}
+
+
+ {formatRelativeTime(notification.createdAt)}
+
+
+
+ {/* 箭头 */}
+
+
+ )}
+
+ );
+ };
+
+ // 渲染空状态
+ const renderEmpty = () => {
+ if (loading.notifications) {
+ return (
+
+
+
+ 加载通知...
+
+
+ );
+ }
+
+ return (
+
+
+
+ 暂无通知
+
+
+ 当有人回复或提及你时会收到通知
+
+
+ );
+ };
+
+ // 渲染加载更多
+ const renderFooter = () => {
+ if (!notificationsHasMore || notifications.length === 0) return null;
+
+ if (loading.notifications) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+ };
+
+ return (
+
+ item?.id || `notification-${index}`}
+ ListHeaderComponent={renderHeader}
+ ListEmptyComponent={renderEmpty}
+ ListFooterComponent={renderFooter}
+ onEndReached={handleLoadMore}
+ onEndReachedThreshold={0.2}
+ refreshControl={
+
+ }
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={[
+ styles.listContent,
+ { paddingBottom: insets.bottom + 20 },
+ ]}
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ listContent: {
+ flexGrow: 1,
+ },
+});
+
+export default Notifications;
diff --git a/MeAgent/src/screens/Community/PostDetail.js b/MeAgent/src/screens/Community/PostDetail.js
new file mode 100644
index 00000000..8efe6701
--- /dev/null
+++ b/MeAgent/src/screens/Community/PostDetail.js
@@ -0,0 +1,510 @@
+/**
+ * 帖子详情页面
+ * 显示帖子内容和回复列表
+ */
+
+import React, { useEffect, useState, useCallback } from 'react';
+import {
+ ScrollView,
+ KeyboardAvoidingView,
+ Platform,
+ StyleSheet,
+ Keyboard,
+ TextInput,
+ Image,
+ Dimensions,
+} from 'react-native';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Icon,
+ Pressable,
+ Spinner,
+ Center,
+ Avatar,
+ Divider,
+} from 'native-base';
+import { Ionicons } from '@expo/vector-icons';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { useDispatch, useSelector } from 'react-redux';
+
+import {
+ fetchPostDetail,
+ fetchReplies,
+ createReply,
+ clearCurrentPost,
+} from '../../store/slices/communitySlice';
+import { postService } from '../../services/communityService';
+
+// 格式化相对时间
+const formatRelativeTime = (dateStr) => {
+ const date = new Date(dateStr);
+ const now = new Date();
+ const diff = now - date;
+
+ const minutes = Math.floor(diff / 60000);
+ const hours = Math.floor(diff / 3600000);
+ const days = Math.floor(diff / 86400000);
+
+ if (minutes < 1) return '刚刚';
+ if (minutes < 60) return `${minutes}分钟前`;
+ if (hours < 24) return `${hours}小时前`;
+ if (days < 30) return `${days}天前`;
+
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ return `${month}月${day}日`;
+};
+
+// 获取屏幕宽度
+const screenWidth = Dimensions.get('window').width;
+
+// 解析 Markdown 内容,提取文本和图片
+const parseMarkdownContent = (content) => {
+ if (!content) return [];
+
+ const parts = [];
+ let lastIndex = 0;
+
+ // 匹配 Markdown 图片格式:  或 
+ const markdownImgRegex = /!\[([^\]]*)\]\((data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+|https?:\/\/[^\s)]+)\)/g;
+
+ let match;
+ while ((match = markdownImgRegex.exec(content)) !== null) {
+ // 添加图片前的文本
+ if (match.index > lastIndex) {
+ const textBefore = content.slice(lastIndex, match.index).trim();
+ if (textBefore) {
+ parts.push({ type: 'text', content: textBefore });
+ }
+ }
+
+ // 添加图片
+ parts.push({
+ type: 'image',
+ uri: match[2],
+ alt: match[1] || '图片',
+ });
+
+ lastIndex = match.index + match[0].length;
+ }
+
+ // 添加最后的文本
+ if (lastIndex < content.length) {
+ const textAfter = content.slice(lastIndex).trim();
+ if (textAfter) {
+ parts.push({ type: 'text', content: textAfter });
+ }
+ }
+
+ // 如果没有匹配到任何图片,返回原始文本
+ if (parts.length === 0 && content.trim()) {
+ parts.push({ type: 'text', content: content });
+ }
+
+ return parts;
+};
+
+const PostDetail = ({ route, navigation }) => {
+ const { post: initialPost, channel } = route.params;
+ const dispatch = useDispatch();
+ const insets = useSafeAreaInsets();
+
+ const { currentPost, replies, loading } = useSelector((state) => state.community);
+ const postReplies = replies[initialPost.id] || [];
+
+ const [replyText, setReplyText] = useState('');
+ const [isSending, setIsSending] = useState(false);
+ const [isLiked, setIsLiked] = useState(false);
+ const [likeCount, setLikeCount] = useState(initialPost.likeCount || 0);
+
+ const post = currentPost || initialPost;
+
+ // 设置导航
+ useEffect(() => {
+ navigation.setOptions({
+ headerShown: true,
+ headerTitle: '',
+ headerStyle: {
+ backgroundColor: '#0F172A',
+ elevation: 0,
+ shadowOpacity: 0,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(255, 255, 255, 0.1)',
+ },
+ headerTintColor: '#FFFFFF',
+ });
+ }, [navigation]);
+
+ // 加载帖子详情和回复
+ useEffect(() => {
+ dispatch(fetchPostDetail(initialPost.id));
+ dispatch(fetchReplies({ postId: initialPost.id }));
+
+ return () => {
+ dispatch(clearCurrentPost());
+ };
+ }, [dispatch, initialPost.id]);
+
+ // 点赞
+ const handleLike = useCallback(async () => {
+ if (isLiked) return;
+
+ setIsLiked(true);
+ setLikeCount((prev) => prev + 1);
+
+ try {
+ await postService.likePost(post.id);
+ } catch (error) {
+ // 回滚
+ setIsLiked(false);
+ setLikeCount((prev) => prev - 1);
+ console.error('点赞失败:', error);
+ }
+ }, [post.id, isLiked]);
+
+ // 发送回复
+ const handleSendReply = useCallback(async () => {
+ const content = replyText.trim();
+ if (!content || isSending) return;
+
+ setIsSending(true);
+ Keyboard.dismiss();
+
+ try {
+ await dispatch(
+ createReply({
+ postId: post.id,
+ data: { content },
+ })
+ ).unwrap();
+
+ setReplyText('');
+ } catch (error) {
+ console.error('回复失败:', error);
+ } finally {
+ setIsSending(false);
+ }
+ }, [dispatch, post.id, replyText, isSending]);
+
+ // 渲染帖子主体
+ const renderPostContent = () => (
+
+ {/* 作者信息 */}
+
+
+ {post.authorName?.[0]?.toUpperCase() || '?'}
+
+
+
+ {post.authorName || '匿名用户'}
+
+
+ {formatRelativeTime(post.createdAt)}
+
+
+
+
+
+
+
+ {/* 标题 */}
+
+ {post.title}
+
+
+ {/* 标签 */}
+ {post.tags && post.tags.length > 0 && (
+
+ {post.tags.map((tag, index) => (
+
+
+ #{tag}
+
+
+ ))}
+
+ )}
+
+ {/* 内容(支持 Markdown 图片) */}
+
+ {parseMarkdownContent(post.content).map((part, index) => {
+ if (part.type === 'text') {
+ return (
+
+ {part.content}
+
+ );
+ }
+ if (part.type === 'image') {
+ return (
+
+
+
+ );
+ }
+ return null;
+ })}
+
+
+ {/* 额外的图片(如果有单独的 images 字段) */}
+ {post.images && post.images.length > 0 ? (
+
+ {post.images.map((image, index) => (
+
+
+
+ ))}
+
+ ) : null}
+
+ {/* 互动栏 */}
+
+
+ {/* 点赞 */}
+
+
+
+
+ {likeCount}
+
+
+
+
+ {/* 回复数 */}
+
+
+
+ {post.replyCount || postReplies.length}
+
+
+
+ {/* 浏览数 */}
+
+
+
+ {post.viewCount || 0}
+
+
+
+
+ {/* 分享 */}
+
+
+
+
+
+ );
+
+ // 渲染回复列表
+ const renderReplies = () => (
+
+ {/* 回复标题 */}
+
+
+
+ 回复 ({postReplies.length})
+
+
+
+ {/* 回复列表 */}
+ {loading.replies ? (
+
+
+
+ ) : postReplies.length === 0 ? (
+
+
+ 暂无回复,来说两句吧
+
+
+ ) : (
+ postReplies.map((reply, index) => (
+
+
+
+ {reply.authorName?.[0]?.toUpperCase() || '?'}
+
+
+
+
+ {reply.authorName || '匿名用户'}
+
+
+ {formatRelativeTime(reply.createdAt)}
+
+
+
+ {reply.content}
+
+
+
+
+
+
+ {reply.likeCount || 0}
+
+
+
+
+
+ 回复
+
+
+
+
+
+ {index < postReplies.length - 1 && (
+
+ )}
+
+ ))
+ )}
+
+ );
+
+ // 渲染回复输入框
+ const renderReplyInput = () => (
+
+
+
+
+
+
+ {isSending ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+
+ return (
+
+
+
+ {/* 帖子内容 */}
+ {renderPostContent()}
+
+
+
+ {/* 回复列表 */}
+ {renderReplies()}
+
+
+
+
+ {/* 回复输入框 */}
+ {renderReplyInput()}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#0F172A',
+ },
+ textInput: {
+ flex: 1,
+ color: '#FFFFFF',
+ fontSize: 14,
+ maxHeight: 100,
+ minHeight: 36,
+ paddingVertical: Platform.OS === 'ios' ? 8 : 10,
+ paddingHorizontal: 0,
+ },
+});
+
+export default PostDetail;
diff --git a/MeAgent/src/screens/Community/index.js b/MeAgent/src/screens/Community/index.js
new file mode 100644
index 00000000..a6d8827c
--- /dev/null
+++ b/MeAgent/src/screens/Community/index.js
@@ -0,0 +1,109 @@
+/**
+ * 社区主入口
+ * Discord 风格的社区论坛
+ */
+
+import React, { useState } from 'react';
+import { Box, HStack, Pressable, Text } from 'native-base';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { StatusBar } from 'react-native';
+import { useSelector } from 'react-redux';
+
+import ChannelList from './ChannelList';
+import Notifications from './Notifications';
+
+// 自定义 Tab Bar
+const CustomTabBar = ({ activeTab, onTabChange, unreadCount }) => {
+ const tabs = [
+ { key: 'channels', label: '频道' },
+ { key: 'notifications', label: '通知', badge: unreadCount },
+ ];
+
+ return (
+
+ {tabs.map((tab) => (
+ onTabChange(tab.key)}
+ >
+
+
+
+ {tab.label}
+
+ {tab.badge > 0 ? (
+
+
+ {tab.badge > 99 ? '99+' : String(tab.badge)}
+
+
+ ) : null}
+
+ {activeTab === tab.key && (
+
+ )}
+
+
+ ))}
+
+ );
+};
+
+const CommunityHome = ({ navigation }) => {
+ const insets = useSafeAreaInsets();
+ const [activeTab, setActiveTab] = useState('channels');
+
+ const communityState = useSelector((state) => state.community);
+ const unreadCount = communityState?.unreadCount || 0;
+
+ return (
+
+
+
+ {/* 自定义 Tab Bar */}
+
+
+ {/* Tab 内容 */}
+
+ {activeTab === 'channels' ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+export default CommunityHome;
+
+// 导出所有社区相关页面
+export { default as ChannelList } from './ChannelList';
+export { default as ChannelDetail } from './ChannelDetail';
+export { default as ForumChannel } from './ForumChannel';
+export { default as PostDetail } from './PostDetail';
+export { default as CreatePost } from './CreatePost';
+export { default as CreateChannel } from './CreateChannel';
+export { default as MemberList } from './MemberList';
+export { default as Notifications } from './Notifications';
diff --git a/MeAgent/src/screens/Market/MarketHot.js b/MeAgent/src/screens/Market/MarketHot.js
index ec925046..d05acac4 100644
--- a/MeAgent/src/screens/Market/MarketHot.js
+++ b/MeAgent/src/screens/Market/MarketHot.js
@@ -368,7 +368,7 @@ const MarketHot = ({ navigation }) => {
{hotSectors.map((sector, index) => {
diff --git a/MeAgent/src/screens/Market/SectorDetail.js b/MeAgent/src/screens/Market/SectorDetail.js
index 72b85a7f..56c244ae 100644
--- a/MeAgent/src/screens/Market/SectorDetail.js
+++ b/MeAgent/src/screens/Market/SectorDetail.js
@@ -93,8 +93,14 @@ const SectorDetail = ({ route, navigation }) => {
// 点击事件
const handleEventPress = useCallback((event) => {
+ // 兼容不同的事件ID字段名: id, event_id
+ const eventId = event.id || event.event_id;
+ if (!eventId) {
+ console.warn('事件ID不存在:', event);
+ return;
+ }
navigation.navigate('EventDetail', {
- eventId: event.id,
+ eventId,
title: event.title,
});
}, [navigation]);
diff --git a/MeAgent/src/services/communityService.js b/MeAgent/src/services/communityService.js
new file mode 100644
index 00000000..480dfdd3
--- /dev/null
+++ b/MeAgent/src/services/communityService.js
@@ -0,0 +1,637 @@
+/**
+ * 社区服务层
+ * 处理频道、消息、帖子、成员等 API 调用
+ * 与 Web 端保持一致,使用 ElasticSearch API 读取数据
+ */
+
+import { apiRequest, API_BASE_URL } from './api';
+
+// ES API 基础路径
+const ES_API_BASE = `${API_BASE_URL}/api/community/es`;
+
+// 频道类型
+export const CHANNEL_TYPES = {
+ TEXT: 'text',
+ FORUM: 'forum',
+ ANNOUNCEMENT: 'announcement',
+};
+
+// 频道分类
+export const CHANNEL_CATEGORIES = [
+ { id: 'general', name: '综合讨论', icon: 'chatbubbles' },
+ { id: 'hot', name: '热门板块', icon: 'flame' },
+ { id: 'announcement', name: '公告频道', icon: 'megaphone' },
+];
+
+/**
+ * 频道服务
+ */
+export const channelService = {
+ /**
+ * 获取频道列表
+ * @returns {Promise
diff --git a/src/views/AgentChat/components/RightSidebar/index.js b/src/views/AgentChat/components/RightSidebar/index.js
index 1bab0e70..b72fe3f1 100644
--- a/src/views/AgentChat/components/RightSidebar/index.js
+++ b/src/views/AgentChat/components/RightSidebar/index.js
@@ -31,15 +31,12 @@ import {
import {
Settings,
ChevronRight,
- Cpu,
Code,
BarChart3,
- Check,
MessageSquare,
Activity,
} from 'lucide-react';
import { animations } from '../../constants/animations';
-import { AVAILABLE_MODELS } from '../../constants/models';
import { MCP_TOOLS, TOOL_CATEGORIES } from '../../constants/tools';
import { GLASS_BLUR } from '@/constants/glassConfig';
@@ -49,8 +46,6 @@ import { GLASS_BLUR } from '@/constants/glassConfig';
* @param {Object} props
* @param {boolean} props.isOpen - 侧边栏是否展开
* @param {Function} props.onClose - 关闭侧边栏回调
- * @param {string} props.selectedModel - 当前选中的模型 ID
- * @param {Function} props.onModelChange - 模型切换回调
* @param {Array} props.selectedTools - 已选工具 ID 列表
* @param {Function} props.onToolsChange - 工具选择变化回调
* @param {number} props.sessionsCount - 会话总数
@@ -60,8 +55,6 @@ import { GLASS_BLUR } from '@/constants/glassConfig';
const RightSidebar = ({
isOpen,
onClose,
- selectedModel,
- onModelChange,
selectedTools,
onToolsChange,
sessionsCount,
@@ -129,19 +122,6 @@ const RightSidebar = ({
-
-
-
- 模型
-
-
- {/* 模型选择 */}
-
-
- {AVAILABLE_MODELS.map((model, idx) => (
-
- onModelChange(model.id)}
- bg={
- selectedModel === model.id
- ? 'rgba(139, 92, 246, 0.15)'
- : 'rgba(255, 255, 255, 0.05)'
- }
- backdropFilter={GLASS_BLUR.md}
- borderWidth={2}
- borderColor={
- selectedModel === model.id ? 'purple.400' : 'rgba(255, 255, 255, 0.1)'
- }
- _hover={{
- borderColor:
- selectedModel === model.id ? 'purple.400' : 'rgba(255, 255, 255, 0.2)',
- boxShadow:
- selectedModel === model.id
- ? '0 8px 20px rgba(139, 92, 246, 0.4)'
- : '0 4px 12px rgba(0, 0, 0, 0.3)',
- }}
- transition="all 0.3s"
- >
-
-
-
- {model.icon}
-
-
-
- {model.name}
-
-
- {model.description}
-
-
- {selectedModel === model.id && (
-
-
-
- )}
-
-
-
-
- ))}
-
-
-
{/* 工具选择 */}
diff --git a/src/views/AgentChat/constants/messageTypes.ts b/src/views/AgentChat/constants/messageTypes.ts
index 58d446cc..6a7c650c 100644
--- a/src/views/AgentChat/constants/messageTypes.ts
+++ b/src/views/AgentChat/constants/messageTypes.ts
@@ -10,6 +10,8 @@ export enum MessageTypes {
USER = 'user',
/** Agent 思考中状态 */
AGENT_THINKING = 'agent_thinking',
+ /** Agent 深度思考过程(可折叠展示) */
+ AGENT_DEEP_THINKING = 'agent_deep_thinking',
/** Agent 执行计划 */
AGENT_PLAN = 'agent_plan',
/** Agent 执行步骤中 */
@@ -64,6 +66,7 @@ export interface AgentMessage extends BaseMessage {
type:
| MessageTypes.AGENT_RESPONSE
| MessageTypes.AGENT_THINKING
+ | MessageTypes.AGENT_DEEP_THINKING
| MessageTypes.AGENT_PLAN
| MessageTypes.AGENT_EXECUTING;
/** 执行计划(JSON 对象)*/
@@ -76,6 +79,10 @@ export interface AgentMessage extends BaseMessage {
execution_time?: number;
error?: string;
}>;
+ /** 深度思考内容(用于 AGENT_DEEP_THINKING 类型)*/
+ thinkingContent?: string;
+ /** 是否正在思考中 */
+ isThinking?: boolean;
/** 额外元数据 */
metadata?: any;
}
diff --git a/src/views/AgentChat/constants/models.ts b/src/views/AgentChat/constants/models.ts
index e21cb948..7131fbf9 100644
--- a/src/views/AgentChat/constants/models.ts
+++ b/src/views/AgentChat/constants/models.ts
@@ -22,34 +22,13 @@ export interface ModelConfig {
/**
* 可用模型配置列表
- * 包含所有可供用户选择的 AI 模型
+ * 当前仅支持 DeepMoney 模型(基于 MiniMax-M2.1)
*/
export const AVAILABLE_MODELS: ModelConfig[] = [
- {
- id: 'deepseek',
- name: 'DeepSeek',
- description: '高性能对话模型,响应迅速',
- icon: React.createElement(Zap, { className: 'w-5 h-5' }),
- color: 'blue',
- },
- {
- id: 'kimi-k2-thinking',
- name: 'Kimi K2 Thinking',
- description: '深度思考模型,适合复杂分析',
- icon: React.createElement(Brain, { className: 'w-5 h-5' }),
- color: 'purple',
- },
- {
- id: 'kimi-k2',
- name: 'Kimi K2',
- description: '快速响应模型,适合简单查询',
- icon: React.createElement(Zap, { className: 'w-5 h-5' }),
- color: 'cyan',
- },
{
id: 'deepmoney',
name: 'DeepMoney',
- description: '金融专业模型,65K 上下文',
+ description: '金融专业模型,84K 上下文,支持深度思考',
icon: React.createElement(TrendingUp, { className: 'w-5 h-5' }),
color: 'green',
},
diff --git a/src/views/AgentChat/hooks/useAgentChat.ts b/src/views/AgentChat/hooks/useAgentChat.ts
index da49dc01..e0fc1544 100644
--- a/src/views/AgentChat/hooks/useAgentChat.ts
+++ b/src/views/AgentChat/hooks/useAgentChat.ts
@@ -122,6 +122,8 @@ export const useAgentChat = ({
const streamStateRef = useRef<{
thinkingContent: string;
summaryContent: string;
+ deepThinkingContent: string; // 深度思考内容(来自 标签)
+ isDeepThinking: boolean; // 是否正在进行深度思考
plan: any;
stepResults: any[];
sessionId: string | null;
@@ -129,6 +131,8 @@ export const useAgentChat = ({
}>({
thinkingContent: '',
summaryContent: '',
+ deepThinkingContent: '',
+ isDeepThinking: false,
plan: null,
stepResults: [],
sessionId: null,
@@ -188,6 +192,8 @@ export const useAgentChat = ({
streamStateRef.current = {
thinkingContent: '',
summaryContent: '',
+ deepThinkingContent: '',
+ isDeepThinking: false,
plan: null,
stepResults: [],
sessionId: null,
@@ -427,6 +433,40 @@ export const useAgentChat = ({
}
break;
+ case 'thinking_start':
+ // 深度思考开始
+ streamStateRef.current.isDeepThinking = true;
+ streamStateRef.current.deepThinkingContent = '';
+ // 移除执行中消息
+ setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING));
+ // 添加深度思考消息
+ addMessage({
+ type: MessageTypes.AGENT_DEEP_THINKING,
+ content: data?.message || '正在深度思考...',
+ thinkingContent: '',
+ isThinking: true,
+ });
+ break;
+
+ case 'thinking_chunk':
+ // 深度思考内容流式输出
+ streamStateRef.current.deepThinkingContent += data?.content || '';
+ updateLastMessage(MessageTypes.AGENT_DEEP_THINKING, {
+ thinkingContent: streamStateRef.current.deepThinkingContent,
+ isThinking: true,
+ });
+ break;
+
+ case 'thinking_end':
+ // 深度思考结束
+ streamStateRef.current.isDeepThinking = false;
+ updateLastMessage(MessageTypes.AGENT_DEEP_THINKING, {
+ thinkingContent: streamStateRef.current.deepThinkingContent,
+ isThinking: false,
+ content: '思考完成',
+ });
+ break;
+
case 'summary_chunk':
// 总结内容流式输出
// 如果还在执行中,先切换到思考状态
diff --git a/src/views/AgentChat/index.js b/src/views/AgentChat/index.js
index 5fb4909f..47317096 100644
--- a/src/views/AgentChat/index.js
+++ b/src/views/AgentChat/index.js
@@ -333,7 +333,6 @@ const AgentChat = () => {
uploadedFiles={uploadedFiles}
onFileSelect={handleFileSelect}
onFileRemove={removeFile}
- selectedModel={selectedModel}
isLeftSidebarOpen={isLeftSidebarOpen}
isRightSidebarOpen={isRightSidebarOpen}
onToggleLeftSidebar={() => setIsLeftSidebarOpen(true)}
@@ -348,8 +347,6 @@ const AgentChat = () => {
setIsRightSidebarOpen(false)}
- selectedModel={selectedModel}
- onModelChange={setSelectedModel}
selectedTools={selectedTools}
onToolsChange={setSelectedTools}
sessionsCount={sessions.length}