diff --git a/src/views/StockCommunity/components/MessageArea/TextChannel/MessageItem.tsx b/src/views/StockCommunity/components/MessageArea/TextChannel/MessageItem.tsx index 974207bc..8793dcda 100644 --- a/src/views/StockCommunity/components/MessageArea/TextChannel/MessageItem.tsx +++ b/src/views/StockCommunity/components/MessageArea/TextChannel/MessageItem.tsx @@ -30,10 +30,20 @@ import { import { format } from 'date-fns'; import { zhCN } from 'date-fns/locale'; -import { Message, Embed } from '../../../types'; +import { Message, Embed, AdminRole } from '../../../types'; +import { useAdmin } from '../../../contexts/AdminContext'; +import { deleteMessage, togglePinMessage } from '../../../services/communityService'; import ReactionBar from './ReactionBar'; import StockEmbed from '../shared/StockEmbed'; +// 角色徽章配置 +const ROLE_BADGE_CONFIG: Record = { + superAdmin: { label: '超管', bg: 'linear-gradient(135deg, rgba(239, 68, 68, 0.3), rgba(220, 38, 38, 0.3))', color: 'red.300' }, + owner: { label: '频主', bg: 'linear-gradient(135deg, rgba(251, 191, 36, 0.3), rgba(245, 158, 11, 0.3))', color: 'yellow.300' }, + admin: { label: '管理', bg: 'linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(37, 99, 235, 0.3))', color: 'blue.300' }, + moderator: { label: '版主', bg: 'linear-gradient(135deg, rgba(34, 197, 94, 0.3), rgba(22, 163, 74, 0.3))', color: 'green.300' }, +}; + interface MessageItemProps { message: Message; showAvatar: boolean; @@ -48,6 +58,30 @@ const MessageItem: React.FC = ({ onThreadOpen, }) => { const [isHovered, setIsHovered] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [isPinning, setIsPinning] = useState(false); + + // 管理员权限 + const { + getUserAdminRole, + isSuperAdmin: currentUserIsSuperAdmin, + canDeleteMessages, + canPinMessages, + globalAdmin + } = useAdmin(); + + // 获取消息作者的管理员角色 + const authorRole = getUserAdminRole(message.authorId); + // 判断作者是否是超级管理员(需要从全局管理员状态判断) + const authorIsSuperAdmin = globalAdmin?.isAdmin && globalAdmin?.role === 'admin' && message.authorId === '65'; + + // 获取显示的角色徽章 + const getRoleBadge = () => { + if (authorIsSuperAdmin) return ROLE_BADGE_CONFIG.superAdmin; + if (authorRole) return ROLE_BADGE_CONFIG[authorRole]; + return null; + }; + const roleBadge = getRoleBadge(); // 深色主题颜色(HeroUI 风格) const hoverBg = 'rgba(255, 255, 255, 0.05)'; @@ -236,6 +270,34 @@ const MessageItem: React.FC = ({ ); }; + // 处理删除消息 + const handleDelete = async () => { + if (isDeleting) return; + setIsDeleting(true); + try { + await deleteMessage(message.id); + // 消息会通过 WebSocket 事件从列表中移除 + } catch (error) { + console.error('删除消息失败:', error); + } finally { + setIsDeleting(false); + } + }; + + // 处理置顶/取消置顶 + const handleTogglePin = async () => { + if (isPinning) return; + setIsPinning(true); + try { + await togglePinMessage(message.id, message.isPinned); + // 消息会通过 WebSocket 事件更新 + } catch (error) { + console.error('置顶操作失败:', error); + } finally { + setIsPinning(false); + } + }; + // 渲染操作栏 const renderActions = () => { if (!isHovered) return null; @@ -300,9 +362,33 @@ const MessageItem: React.FC = ({ borderColor="rgba(255, 255, 255, 0.1)" boxShadow="0 10px 40px rgba(0, 0, 0, 0.4)" > - } bg="transparent" _hover={{ bg: 'whiteAlpha.100' }} color="gray.300">置顶消息 + {/* 置顶消息 - 需要管理权限 */} + {canPinMessages && ( + } + bg="transparent" + _hover={{ bg: 'whiteAlpha.100' }} + color="gray.300" + onClick={handleTogglePin} + isDisabled={isPinning} + > + {message.isPinned ? '取消置顶' : '置顶消息'} + + )} } bg="transparent" _hover={{ bg: 'whiteAlpha.100' }} color="gray.300">编辑 - } bg="transparent" _hover={{ bg: 'rgba(239, 68, 68, 0.2)' }} color="red.400">删除 + {/* 删除消息 - 需要管理权限 */} + {canDeleteMessages && ( + } + bg="transparent" + _hover={{ bg: 'rgba(239, 68, 68, 0.2)' }} + color="red.400" + onClick={handleDelete} + isDisabled={isDeleting} + > + {isDeleting ? '删除中...' : '删除'} + + )} @@ -345,13 +431,34 @@ const MessageItem: React.FC = ({ {message.authorName} + {/* 管理员角色徽章 */} + {roleBadge && ( + + {roleBadge.label} + + )} {formatTime(message.createdAt)} {message.isPinned && ( - + 置顶 )} diff --git a/src/views/StockCommunity/components/MessageArea/TextChannel/index.tsx b/src/views/StockCommunity/components/MessageArea/TextChannel/index.tsx index 2fafa0c4..b9bfa641 100644 --- a/src/views/StockCommunity/components/MessageArea/TextChannel/index.tsx +++ b/src/views/StockCommunity/components/MessageArea/TextChannel/index.tsx @@ -20,8 +20,9 @@ import { motion } from 'framer-motion'; import { Pin, Search, Users, Hash, Bell, Settings } from 'lucide-react'; import { Channel, Message } from '../../../types'; -import { getMessages } from '../../../services/communityService'; +import { getMessages, getChannelAdmins } from '../../../services/communityService'; import { useCommunitySocket } from '../../../hooks/useCommunitySocket'; +import { useAdmin } from '../../../contexts/AdminContext'; import MessageList from './MessageList'; import MessageInput from './MessageInput'; import { GLASS_BLUR } from '@/constants/glassConfig'; @@ -48,6 +49,9 @@ const TextChannel: React.FC = ({ const messagesEndRef = useRef(null); const containerRef = useRef(null); + // 管理员状态 + const { loadChannelAdminStatus, setChannelAdminList } = useAdmin(); + // WebSocket const { onMessage, offMessage } = useCommunitySocket(); @@ -80,7 +84,17 @@ const TextChannel: React.FC = ({ // 初始加载 useEffect(() => { loadMessages(); - }, [loadMessages]); + // 加载管理员状态 + loadChannelAdminStatus(channel.id); + // 加载频道管理员列表 + getChannelAdmins(channel.id).then(admins => { + const adminMap = new Map(); + admins.forEach(admin => { + adminMap.set(admin.userId, admin.role); + }); + setChannelAdminList(adminMap); + }).catch(console.error); + }, [loadMessages, channel.id, loadChannelAdminStatus, setChannelAdminList]); // 监听新消息 useEffect(() => { diff --git a/src/views/StockCommunity/components/RightPanel/MemberList.tsx b/src/views/StockCommunity/components/RightPanel/MemberList.tsx index 56ef87df..0368726b 100644 --- a/src/views/StockCommunity/components/RightPanel/MemberList.tsx +++ b/src/views/StockCommunity/components/RightPanel/MemberList.tsx @@ -15,10 +15,20 @@ import { InputLeftElement, Icon, } from '@chakra-ui/react'; -import { Search } from 'lucide-react'; +import { Search, Crown, Shield, UserCheck } from 'lucide-react'; import { motion } from 'framer-motion'; -import { CommunityMember } from '../../types'; +import { CommunityMember, AdminRole, ChannelAdmin } from '../../types'; +import { useAdmin } from '../../contexts/AdminContext'; +import { getChannelAdmins } from '../../services/communityService'; + +// 角色配置 +const ROLE_CONFIG: Record = { + superAdmin: { icon: Crown, label: '超级管理员', color: 'red.300', bg: 'rgba(239, 68, 68, 0.2)' }, + owner: { icon: Crown, label: '频道创建者', color: 'yellow.300', bg: 'rgba(251, 191, 36, 0.2)' }, + admin: { icon: Shield, label: '管理员', color: 'blue.300', bg: 'rgba(59, 130, 246, 0.2)' }, + moderator: { icon: UserCheck, label: '版主', color: 'green.300', bg: 'rgba(34, 197, 94, 0.2)' }, +}; interface MemberListProps { channelId: string; @@ -40,20 +50,39 @@ const MemberList: React.FC = ({ channelId }) => { const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); + const [channelAdmins, setChannelAdmins] = useState([]); - // 加载成员 + // 获取管理员上下文 + const { isSuperAdmin, channelAdminList } = useAdmin(); + + // 加载成员和管理员列表 useEffect(() => { - const loadMembers = async () => { + const loadData = async () => { setLoading(true); - // 模拟加载 + try { + // 加载频道管理员列表 + const admins = await getChannelAdmins(channelId); + setChannelAdmins(admins); + } catch (error) { + console.error('加载频道管理员失败:', error); + } + // 模拟加载成员 await new Promise(resolve => setTimeout(resolve, 500)); setMembers(mockMembers); setLoading(false); }; - loadMembers(); + loadData(); }, [channelId]); + // 获取成员的管理员角色 + const getMemberRole = (userId: string): AdminRole | 'superAdmin' | null => { + // 检查是否是超级管理员(ID=65) + if (userId === '65') return 'superAdmin'; + // 从管理员列表中查找 + return channelAdminList.get(userId) || null; + }; + // 过滤成员 const filteredMembers = searchTerm ? members.filter(m => m.username.toLowerCase().includes(searchTerm.toLowerCase())) @@ -64,72 +93,109 @@ const MemberList: React.FC = ({ channelId }) => { const offlineMembers = filteredMembers.filter(m => !m.isOnline); // 渲染成员项 - const renderMember = (member: CommunityMember, index: number) => ( - - - - - {member.isOnline && ( - - )} - + const renderMember = (member: CommunityMember, index: number) => { + const role = getMemberRole(member.userId); + const roleConfig = role ? ROLE_CONFIG[role] : null; - - - - {member.username} - - {member.badge && ( - + + + + {member.isOnline && ( + - {member.badge} - + borderWidth="2px" + borderColor="rgba(17, 24, 39, 0.95)" + boxShadow="0 0 8px rgba(74, 222, 128, 0.6)" + /> )} - - {member.level && ( - - Lv.{member.level} - - )} - - - - ); + {/* 管理员角色图标 */} + {roleConfig && ( + + + + )} + + + + + + {member.username} + + {/* 管理员角色徽章 */} + {roleConfig && ( + + {roleConfig.label} + + )} + {/* 用户徽章(大V等) */} + {!roleConfig && member.badge && ( + + {member.badge} + + )} + + {member.level && ( + + Lv.{member.level} + + )} + + + + ); + }; if (loading) { return ( @@ -188,9 +254,86 @@ const MemberList: React.FC = ({ channelId }) => { + {/* 频道管理员分组 */} + {channelAdmins.length > 0 && ( + + + + + 管理团队 — {channelAdmins.length} + + + + {channelAdmins.map((admin, index) => { + const roleConfig = ROLE_CONFIG[admin.role]; + return ( + + + + + + + + + + + + {admin.username} + + + {roleConfig.label} + + + + + + ); + })} + + + )} + {/* 在线成员 */} {onlineMembers.length > 0 && ( - + 0 ? 4 : 0}> Promise; + refreshGlobalAdminStatus: () => Promise; + // 根据用户 ID 获取其在当前频道的角色 + getUserAdminRole: (authorId: string) => AdminRole | null; + channelAdminList: Map; + setChannelAdminList: (list: Map) => void; +} + +const AdminContext = createContext(undefined); + +interface AdminProviderProps { + children: ReactNode; +} + +export const AdminProvider: React.FC = ({ children }) => { + const [globalAdmin, setGlobalAdmin] = useState(null); + const [channelAdmin, setChannelAdmin] = useState(null); + const [currentChannelId, setCurrentChannelId] = useState(null); + const [loading, setLoading] = useState(false); + const [channelAdminList, setChannelAdminList] = useState>(new Map()); + + // 加载全局管理员状态 + const refreshGlobalAdminStatus = useCallback(async () => { + try { + const status = await getGlobalAdminStatus(); + setGlobalAdmin(status); + } catch (error) { + console.error('获取全局管理员状态失败:', error); + setGlobalAdmin({ isAdmin: false, permissions: {} }); + } + }, []); + + // 加载频道管理员状态 + const loadChannelAdminStatus = useCallback(async (channelId: string) => { + if (!channelId) return; + + setLoading(true); + setCurrentChannelId(channelId); + + try { + const status = await getChannelAdminStatus(channelId); + setChannelAdmin(status); + } catch (error) { + console.error('获取频道管理员状态失败:', error); + setChannelAdmin({ isAdmin: false, isSuperAdmin: false, isOwner: false, permissions: {} }); + } finally { + setLoading(false); + } + }, []); + + // 初始加载全局管理员状态 + useEffect(() => { + refreshGlobalAdminStatus(); + }, [refreshGlobalAdminStatus]); + + // 根据用户 ID 获取其在当前频道的角色 + const getUserAdminRole = useCallback((authorId: string): AdminRole | null => { + return channelAdminList.get(authorId) || null; + }, [channelAdminList]); + + // 计算权限快捷属性 + const isSuperAdmin = globalAdmin?.isAdmin === true && globalAdmin?.role === 'admin'; + const isChannelOwner = channelAdmin?.isOwner === true; + const isChannelAdmin = channelAdmin?.isAdmin === true; + const canDeleteMessages = isSuperAdmin || channelAdmin?.permissions?.delete_messages === true; + const canPinMessages = isSuperAdmin || channelAdmin?.permissions?.pin_messages === true; + const canManageAdmins = isSuperAdmin || channelAdmin?.permissions?.manage_admins === true; + const canEditChannel = isSuperAdmin || channelAdmin?.permissions?.edit_channel === true; + + const value: AdminContextValue = { + globalAdmin, + channelAdmin, + currentChannelId, + loading, + isSuperAdmin, + isChannelOwner, + isChannelAdmin, + canDeleteMessages, + canPinMessages, + canManageAdmins, + canEditChannel, + loadChannelAdminStatus, + refreshGlobalAdminStatus, + getUserAdminRole, + channelAdminList, + setChannelAdminList, + }; + + return ( + + {children} + + ); +}; + +export const useAdmin = (): AdminContextValue => { + const context = useContext(AdminContext); + if (!context) { + throw new Error('useAdmin must be used within AdminProvider'); + } + return context; +}; + +export default AdminContext; diff --git a/src/views/StockCommunity/index.tsx b/src/views/StockCommunity/index.tsx index f7f5014c..8f1ce5ba 100644 --- a/src/views/StockCommunity/index.tsx +++ b/src/views/StockCommunity/index.tsx @@ -33,6 +33,7 @@ import MessageArea from './components/MessageArea'; import RightPanel from './components/RightPanel'; import PredictionMarket from './components/PredictionMarket'; import { useCommunitySocket } from './hooks/useCommunitySocket'; +import { AdminProvider } from './contexts/AdminContext'; import { Channel } from './types'; import { GLASS_BLUR, GLASS_BG, GLASS_BORDER, GLASS_SHADOW } from '@/constants/glassConfig'; @@ -211,6 +212,7 @@ const StockCommunity: React.FC = () => { ); return ( + { + ); }; diff --git a/src/views/StockCommunity/services/communityService.ts b/src/views/StockCommunity/services/communityService.ts index 9263ce26..de87dd40 100644 --- a/src/views/StockCommunity/services/communityService.ts +++ b/src/views/StockCommunity/services/communityService.ts @@ -10,6 +10,9 @@ import { ForumPost, ForumReply, PaginatedResponse, + ChannelAdminInfo, + GlobalAdminInfo, + ChannelAdmin, } from '../types'; const API_BASE = getApiBase(); @@ -490,3 +493,97 @@ export const search = async (options: SearchOptions): Promise => { + const response = await fetch(`${API_BASE}/api/community/me/admin-status`, { + credentials: 'include', + }); + if (!response.ok) { + return { isAdmin: false, permissions: {} }; + } + const data = await response.json(); + return data.data; +}; + +/** + * 获取当前用户在指定频道的管理员状态 + */ +export const getChannelAdminStatus = async (channelId: string): Promise => { + const response = await fetch(`${API_BASE}/api/community/channels/${channelId}/admin-status`, { + credentials: 'include', + }); + if (!response.ok) { + return { isAdmin: false, isSuperAdmin: false, isOwner: false, permissions: {} }; + } + const data = await response.json(); + return data.data; +}; + +/** + * 获取频道管理员列表 + */ +export const getChannelAdmins = async (channelId: string): Promise => { + const response = await fetch(`${API_BASE}/api/community/channels/${channelId}/admins`, { + credentials: 'include', + }); + if (!response.ok) throw new Error('获取频道管理员列表失败'); + const data = await response.json(); + return data.data; +}; + +/** + * 添加频道管理员 + */ +export const addChannelAdmin = async ( + channelId: string, + userId: string, + role: 'admin' | 'moderator' +): Promise => { + const response = await fetch(`${API_BASE}/api/community/channels/${channelId}/admins`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ userId, role }), + }); + if (!response.ok) throw new Error('添加管理员失败'); +}; + +/** + * 移除频道管理员 + */ +export const removeChannelAdmin = async (channelId: string, userId: string): Promise => { + const response = await fetch(`${API_BASE}/api/community/channels/${channelId}/admins/${userId}`, { + method: 'DELETE', + credentials: 'include', + }); + if (!response.ok) throw new Error('移除管理员失败'); +}; + +/** + * 删除消息(管理员功能) + */ +export const deleteMessage = async (messageId: string): Promise => { + const response = await fetch(`${API_BASE}/api/community/messages/${messageId}`, { + method: 'DELETE', + credentials: 'include', + }); + if (!response.ok) throw new Error('删除消息失败'); +}; + +/** + * 置顶/取消置顶消息(管理员功能) + */ +export const togglePinMessage = async (messageId: string, isPinned: boolean): Promise => { + const response = await fetch(`${API_BASE}/api/community/messages/${messageId}/pin`, { + method: isPinned ? 'DELETE' : 'POST', + credentials: 'include', + }); + if (!response.ok) throw new Error(isPinned ? '取消置顶失败' : '置顶失败'); +}; diff --git a/src/views/StockCommunity/types/index.ts b/src/views/StockCommunity/types/index.ts index 2a4546fe..dee83fb7 100644 --- a/src/views/StockCommunity/types/index.ts +++ b/src/views/StockCommunity/types/index.ts @@ -275,3 +275,42 @@ export interface ApiResponse { message: string; data: T; } + +// ============================================================ +// 管理员相关 +// ============================================================ + +export type AdminRole = 'owner' | 'admin' | 'moderator'; + +export interface AdminPermissions { + delete_channel?: boolean; + edit_channel?: boolean; + manage_admins?: boolean; + pin_messages?: boolean; + delete_messages?: boolean; + slow_mode?: boolean; + lock_channel?: boolean; +} + +export interface ChannelAdminInfo { + isAdmin: boolean; + isSuperAdmin: boolean; + isOwner: boolean; + role?: AdminRole; + permissions: AdminPermissions; +} + +export interface GlobalAdminInfo { + isAdmin: boolean; + role?: 'admin' | 'moderator'; + permissions: Record; +} + +export interface ChannelAdmin { + userId: string; + username: string; + avatar?: string; + role: AdminRole; + permissions: AdminPermissions; + createdAt: string; +}