个股论坛重做
This commit is contained in:
@@ -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<AdminRole | 'superAdmin', { label: string; bg: string; color: string }> = {
|
||||
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<MessageItemProps> = ({
|
||||
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<MessageItemProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// 处理删除消息
|
||||
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<MessageItemProps> = ({
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
boxShadow="0 10px 40px rgba(0, 0, 0, 0.4)"
|
||||
>
|
||||
<MenuItem icon={<MdPushPin />} bg="transparent" _hover={{ bg: 'whiteAlpha.100' }} color="gray.300">置顶消息</MenuItem>
|
||||
{/* 置顶消息 - 需要管理权限 */}
|
||||
{canPinMessages && (
|
||||
<MenuItem
|
||||
icon={<MdPushPin />}
|
||||
bg="transparent"
|
||||
_hover={{ bg: 'whiteAlpha.100' }}
|
||||
color="gray.300"
|
||||
onClick={handleTogglePin}
|
||||
isDisabled={isPinning}
|
||||
>
|
||||
{message.isPinned ? '取消置顶' : '置顶消息'}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem icon={<MdEdit />} bg="transparent" _hover={{ bg: 'whiteAlpha.100' }} color="gray.300">编辑</MenuItem>
|
||||
<MenuItem icon={<MdDelete />} bg="transparent" _hover={{ bg: 'rgba(239, 68, 68, 0.2)' }} color="red.400">删除</MenuItem>
|
||||
{/* 删除消息 - 需要管理权限 */}
|
||||
{canDeleteMessages && (
|
||||
<MenuItem
|
||||
icon={<MdDelete />}
|
||||
bg="transparent"
|
||||
_hover={{ bg: 'rgba(239, 68, 68, 0.2)' }}
|
||||
color="red.400"
|
||||
onClick={handleDelete}
|
||||
isDisabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? '删除中...' : '删除'}
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</HStack>
|
||||
@@ -345,13 +431,34 @@ const MessageItem: React.FC<MessageItemProps> = ({
|
||||
<Text fontWeight="semibold" color={textColor}>
|
||||
{message.authorName}
|
||||
</Text>
|
||||
{/* 管理员角色徽章 */}
|
||||
{roleBadge && (
|
||||
<Badge
|
||||
bg={roleBadge.bg}
|
||||
color={roleBadge.color}
|
||||
fontSize="2xs"
|
||||
px={1.5}
|
||||
py={0.5}
|
||||
borderRadius="md"
|
||||
fontWeight="bold"
|
||||
textTransform="none"
|
||||
>
|
||||
{roleBadge.label}
|
||||
</Badge>
|
||||
)}
|
||||
<Tooltip label={formatFullTime(message.createdAt)}>
|
||||
<Text fontSize="xs" color={mutedColor}>
|
||||
{formatTime(message.createdAt)}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
{message.isPinned && (
|
||||
<Badge colorScheme="blue" fontSize="xs">
|
||||
<Badge
|
||||
bg="rgba(59, 130, 246, 0.2)"
|
||||
color="blue.300"
|
||||
fontSize="2xs"
|
||||
px={1.5}
|
||||
borderRadius="md"
|
||||
>
|
||||
置顶
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -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<TextChannelProps> = ({
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 管理员状态
|
||||
const { loadChannelAdminStatus, setChannelAdminList } = useAdmin();
|
||||
|
||||
// WebSocket
|
||||
const { onMessage, offMessage } = useCommunitySocket();
|
||||
|
||||
@@ -80,7 +84,17 @@ const TextChannel: React.FC<TextChannelProps> = ({
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
loadMessages();
|
||||
}, [loadMessages]);
|
||||
// 加载管理员状态
|
||||
loadChannelAdminStatus(channel.id);
|
||||
// 加载频道管理员列表
|
||||
getChannelAdmins(channel.id).then(admins => {
|
||||
const adminMap = new Map<string, 'owner' | 'admin' | 'moderator'>();
|
||||
admins.forEach(admin => {
|
||||
adminMap.set(admin.userId, admin.role);
|
||||
});
|
||||
setChannelAdminList(adminMap);
|
||||
}).catch(console.error);
|
||||
}, [loadMessages, channel.id, loadChannelAdminStatus, setChannelAdminList]);
|
||||
|
||||
// 监听新消息
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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<AdminRole | 'superAdmin', { icon: any; label: string; color: string; bg: string }> = {
|
||||
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<MemberListProps> = ({ channelId }) => {
|
||||
const [members, setMembers] = useState<CommunityMember[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [channelAdmins, setChannelAdmins] = useState<ChannelAdmin[]>([]);
|
||||
|
||||
// 加载成员
|
||||
// 获取管理员上下文
|
||||
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<MemberListProps> = ({ channelId }) => {
|
||||
const offlineMembers = filteredMembers.filter(m => !m.isOnline);
|
||||
|
||||
// 渲染成员项
|
||||
const renderMember = (member: CommunityMember, index: number) => (
|
||||
<motion.div
|
||||
key={member.userId}
|
||||
initial={{ opacity: 0, x: 10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.03 }}
|
||||
>
|
||||
<HStack
|
||||
px={3}
|
||||
py={2}
|
||||
cursor="pointer"
|
||||
borderRadius="lg"
|
||||
_hover={{ bg: 'whiteAlpha.100' }}
|
||||
opacity={member.isOnline ? 1 : 0.5}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<Box position="relative">
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={member.username}
|
||||
src={member.avatar}
|
||||
bg="linear-gradient(135deg, rgba(139, 92, 246, 0.6), rgba(59, 130, 246, 0.6))"
|
||||
/>
|
||||
{member.isOnline && (
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
right={0}
|
||||
w="10px"
|
||||
h="10px"
|
||||
bg="green.400"
|
||||
borderRadius="full"
|
||||
borderWidth="2px"
|
||||
borderColor="rgba(17, 24, 39, 0.95)"
|
||||
boxShadow="0 0 8px rgba(74, 222, 128, 0.6)"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
const renderMember = (member: CommunityMember, index: number) => {
|
||||
const role = getMemberRole(member.userId);
|
||||
const roleConfig = role ? ROLE_CONFIG[role] : null;
|
||||
|
||||
<Box flex={1}>
|
||||
<HStack spacing={1}>
|
||||
<Text fontSize="sm" fontWeight="medium" color="white">
|
||||
{member.username}
|
||||
</Text>
|
||||
{member.badge && (
|
||||
<Badge
|
||||
size="sm"
|
||||
bg={member.badge === '大V' ? 'rgba(251, 191, 36, 0.2)' : 'rgba(59, 130, 246, 0.2)'}
|
||||
color={member.badge === '大V' ? 'yellow.300' : 'blue.300'}
|
||||
fontSize="xs"
|
||||
px={1.5}
|
||||
return (
|
||||
<motion.div
|
||||
key={member.userId}
|
||||
initial={{ opacity: 0, x: 10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.03 }}
|
||||
>
|
||||
<HStack
|
||||
px={3}
|
||||
py={2}
|
||||
cursor="pointer"
|
||||
borderRadius="lg"
|
||||
_hover={{ bg: 'whiteAlpha.100' }}
|
||||
opacity={member.isOnline ? 1 : 0.5}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<Box position="relative">
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={member.username}
|
||||
src={member.avatar}
|
||||
bg={roleConfig
|
||||
? `linear-gradient(135deg, ${roleConfig.bg}, ${roleConfig.bg})`
|
||||
: 'linear-gradient(135deg, rgba(139, 92, 246, 0.6), rgba(59, 130, 246, 0.6))'
|
||||
}
|
||||
/>
|
||||
{member.isOnline && (
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
right={0}
|
||||
w="10px"
|
||||
h="10px"
|
||||
bg="green.400"
|
||||
borderRadius="full"
|
||||
>
|
||||
{member.badge}
|
||||
</Badge>
|
||||
borderWidth="2px"
|
||||
borderColor="rgba(17, 24, 39, 0.95)"
|
||||
boxShadow="0 0 8px rgba(74, 222, 128, 0.6)"
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
{member.level && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
Lv.{member.level}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</HStack>
|
||||
</motion.div>
|
||||
);
|
||||
{/* 管理员角色图标 */}
|
||||
{roleConfig && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={-1}
|
||||
right={-1}
|
||||
p={0.5}
|
||||
bg={roleConfig.bg}
|
||||
borderRadius="full"
|
||||
borderWidth="1px"
|
||||
borderColor="rgba(17, 24, 39, 0.95)"
|
||||
>
|
||||
<Icon as={roleConfig.icon} boxSize={2.5} color={roleConfig.color} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flex={1}>
|
||||
<HStack spacing={1}>
|
||||
<Text fontSize="sm" fontWeight="medium" color={roleConfig ? roleConfig.color : 'white'}>
|
||||
{member.username}
|
||||
</Text>
|
||||
{/* 管理员角色徽章 */}
|
||||
{roleConfig && (
|
||||
<Badge
|
||||
size="sm"
|
||||
bg={roleConfig.bg}
|
||||
color={roleConfig.color}
|
||||
fontSize="2xs"
|
||||
px={1}
|
||||
borderRadius="md"
|
||||
>
|
||||
{roleConfig.label}
|
||||
</Badge>
|
||||
)}
|
||||
{/* 用户徽章(大V等) */}
|
||||
{!roleConfig && member.badge && (
|
||||
<Badge
|
||||
size="sm"
|
||||
bg={member.badge === '大V' ? 'rgba(251, 191, 36, 0.2)' : 'rgba(59, 130, 246, 0.2)'}
|
||||
color={member.badge === '大V' ? 'yellow.300' : 'blue.300'}
|
||||
fontSize="xs"
|
||||
px={1.5}
|
||||
borderRadius="full"
|
||||
>
|
||||
{member.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
{member.level && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
Lv.{member.level}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</HStack>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -188,9 +254,86 @@ const MemberList: React.FC<MemberListProps> = ({ channelId }) => {
|
||||
</InputGroup>
|
||||
</Box>
|
||||
|
||||
{/* 频道管理员分组 */}
|
||||
{channelAdmins.length > 0 && (
|
||||
<Box>
|
||||
<HStack px={3} py={2} spacing={2}>
|
||||
<Icon as={Crown} boxSize={3} color="yellow.400" />
|
||||
<Text
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
color="yellow.400"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
管理团队 — {channelAdmins.length}
|
||||
</Text>
|
||||
</HStack>
|
||||
<VStack spacing={0} align="stretch">
|
||||
{channelAdmins.map((admin, index) => {
|
||||
const roleConfig = ROLE_CONFIG[admin.role];
|
||||
return (
|
||||
<motion.div
|
||||
key={admin.userId}
|
||||
initial={{ opacity: 0, x: 10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.03 }}
|
||||
>
|
||||
<HStack
|
||||
px={3}
|
||||
py={2}
|
||||
cursor="pointer"
|
||||
borderRadius="lg"
|
||||
_hover={{ bg: 'whiteAlpha.100' }}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<Box position="relative">
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={admin.username}
|
||||
src={admin.avatar}
|
||||
bg={`linear-gradient(135deg, ${roleConfig.bg}, ${roleConfig.bg})`}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
top={-1}
|
||||
right={-1}
|
||||
p={0.5}
|
||||
bg={roleConfig.bg}
|
||||
borderRadius="full"
|
||||
borderWidth="1px"
|
||||
borderColor="rgba(17, 24, 39, 0.95)"
|
||||
>
|
||||
<Icon as={roleConfig.icon} boxSize={2.5} color={roleConfig.color} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<HStack spacing={1}>
|
||||
<Text fontSize="sm" fontWeight="medium" color={roleConfig.color}>
|
||||
{admin.username}
|
||||
</Text>
|
||||
<Badge
|
||||
size="sm"
|
||||
bg={roleConfig.bg}
|
||||
color={roleConfig.color}
|
||||
fontSize="2xs"
|
||||
px={1}
|
||||
borderRadius="md"
|
||||
>
|
||||
{roleConfig.label}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</Box>
|
||||
</HStack>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 在线成员 */}
|
||||
{onlineMembers.length > 0 && (
|
||||
<Box>
|
||||
<Box mt={channelAdmins.length > 0 ? 4 : 0}>
|
||||
<Text
|
||||
px={3}
|
||||
py={2}
|
||||
|
||||
126
src/views/StockCommunity/contexts/AdminContext.tsx
Normal file
126
src/views/StockCommunity/contexts/AdminContext.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 管理员权限 Context
|
||||
* 在组件树中共享管理员状态
|
||||
*/
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
||||
import { ChannelAdminInfo, GlobalAdminInfo, AdminRole } from '../types';
|
||||
import { getGlobalAdminStatus, getChannelAdminStatus } from '../services/communityService';
|
||||
|
||||
interface AdminContextValue {
|
||||
globalAdmin: GlobalAdminInfo | null;
|
||||
channelAdmin: ChannelAdminInfo | null;
|
||||
currentChannelId: string | null;
|
||||
loading: boolean;
|
||||
// 快捷方法
|
||||
isSuperAdmin: boolean;
|
||||
isChannelOwner: boolean;
|
||||
isChannelAdmin: boolean;
|
||||
canDeleteMessages: boolean;
|
||||
canPinMessages: boolean;
|
||||
canManageAdmins: boolean;
|
||||
canEditChannel: boolean;
|
||||
// 操作方法
|
||||
loadChannelAdminStatus: (channelId: string) => Promise<void>;
|
||||
refreshGlobalAdminStatus: () => Promise<void>;
|
||||
// 根据用户 ID 获取其在当前频道的角色
|
||||
getUserAdminRole: (authorId: string) => AdminRole | null;
|
||||
channelAdminList: Map<string, AdminRole>;
|
||||
setChannelAdminList: (list: Map<string, AdminRole>) => void;
|
||||
}
|
||||
|
||||
const AdminContext = createContext<AdminContextValue | undefined>(undefined);
|
||||
|
||||
interface AdminProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const AdminProvider: React.FC<AdminProviderProps> = ({ children }) => {
|
||||
const [globalAdmin, setGlobalAdmin] = useState<GlobalAdminInfo | null>(null);
|
||||
const [channelAdmin, setChannelAdmin] = useState<ChannelAdminInfo | null>(null);
|
||||
const [currentChannelId, setCurrentChannelId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [channelAdminList, setChannelAdminList] = useState<Map<string, AdminRole>>(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 (
|
||||
<AdminContext.Provider value={value}>
|
||||
{children}
|
||||
</AdminContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAdmin = (): AdminContextValue => {
|
||||
const context = useContext(AdminContext);
|
||||
if (!context) {
|
||||
throw new Error('useAdmin must be used within AdminProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default AdminContext;
|
||||
@@ -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 (
|
||||
<AdminProvider>
|
||||
<Box
|
||||
h="calc(100vh - 60px)"
|
||||
bg="gray.900"
|
||||
@@ -373,6 +375,7 @@ const StockCommunity: React.FC = () => {
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</AdminProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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<PaginatedResponse<
|
||||
hasMore: page * pageSize < data.hits.total.value,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 管理员相关
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 获取当前用户的全局管理员状态
|
||||
*/
|
||||
export const getGlobalAdminStatus = async (): Promise<GlobalAdminInfo> => {
|
||||
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<ChannelAdminInfo> => {
|
||||
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<ChannelAdmin[]> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
const response = await fetch(`${API_BASE}/api/community/messages/${messageId}/pin`, {
|
||||
method: isPinned ? 'DELETE' : 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) throw new Error(isPinned ? '取消置顶失败' : '置顶失败');
|
||||
};
|
||||
|
||||
@@ -275,3 +275,42 @@ export interface ApiResponse<T> {
|
||||
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<string, boolean>;
|
||||
}
|
||||
|
||||
export interface ChannelAdmin {
|
||||
userId: string;
|
||||
username: string;
|
||||
avatar?: string;
|
||||
role: AdminRole;
|
||||
permissions: AdminPermissions;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user