个股论坛重做

This commit is contained in:
2026-01-06 12:55:35 +08:00
parent 915bfca3d6
commit 532dbc343d
7 changed files with 605 additions and 76 deletions

View File

@@ -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>
)}

View File

@@ -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(() => {

View File

@@ -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}

View 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;

View File

@@ -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>
);
};

View File

@@ -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 ? '取消置顶失败' : '置顶失败');
};

View File

@@ -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;
}