个股论坛重做
This commit is contained in:
@@ -1226,8 +1226,10 @@ def allowed_file(filename):
|
|||||||
def upload_image():
|
def upload_image():
|
||||||
"""
|
"""
|
||||||
上传图片
|
上传图片
|
||||||
返回图片 URL,可用于帖子内容中
|
返回 base64 编码的图片数据,可直接嵌入帖子内容中存储到 ES
|
||||||
"""
|
"""
|
||||||
|
import base64
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if 'file' not in request.files:
|
if 'file' not in request.files:
|
||||||
return api_error('没有选择文件', 400)
|
return api_error('没有选择文件', 400)
|
||||||
@@ -1240,35 +1242,36 @@ def upload_image():
|
|||||||
if not allowed_file(file.filename):
|
if not allowed_file(file.filename):
|
||||||
return api_error('不支持的文件格式,仅支持 PNG、JPG、GIF、WebP', 400)
|
return api_error('不支持的文件格式,仅支持 PNG、JPG、GIF、WebP', 400)
|
||||||
|
|
||||||
# 检查文件大小
|
# 读取文件内容
|
||||||
file.seek(0, 2) # 移动到文件末尾
|
file_content = file.read()
|
||||||
size = file.tell()
|
size = len(file_content)
|
||||||
file.seek(0) # 回到开头
|
|
||||||
|
|
||||||
if size > MAX_FILE_SIZE:
|
if size > MAX_FILE_SIZE:
|
||||||
return api_error('文件大小超过限制(最大 10MB)', 400)
|
return api_error('文件大小超过限制(最大 10MB)', 400)
|
||||||
|
|
||||||
# 生成唯一文件名
|
# 获取文件扩展名和 MIME 类型
|
||||||
ext = file.filename.rsplit('.', 1)[1].lower()
|
ext = file.filename.rsplit('.', 1)[1].lower()
|
||||||
|
mime_types = {
|
||||||
|
'png': 'image/png',
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'jpeg': 'image/jpeg',
|
||||||
|
'gif': 'image/gif',
|
||||||
|
'webp': 'image/webp'
|
||||||
|
}
|
||||||
|
mime_type = mime_types.get(ext, 'image/jpeg')
|
||||||
|
|
||||||
|
# 转换为 base64
|
||||||
|
base64_data = base64.b64encode(file_content).decode('utf-8')
|
||||||
|
data_url = f"data:{mime_type};base64,{base64_data}"
|
||||||
|
|
||||||
|
# 生成唯一标识
|
||||||
filename = f"{generate_id()}.{ext}"
|
filename = f"{generate_id()}.{ext}"
|
||||||
|
|
||||||
# 创建日期目录
|
|
||||||
today = datetime.now().strftime('%Y%m%d')
|
|
||||||
upload_dir = os.path.join(current_app.root_path, UPLOAD_FOLDER, today)
|
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# 保存文件
|
|
||||||
filepath = os.path.join(upload_dir, filename)
|
|
||||||
file.save(filepath)
|
|
||||||
|
|
||||||
# 返回访问 URL
|
|
||||||
url = f"/uploads/community/{today}/{filename}"
|
|
||||||
|
|
||||||
return api_response({
|
return api_response({
|
||||||
'url': url,
|
'url': data_url, # base64 data URL,可直接用于 img src
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
'size': size,
|
'size': size,
|
||||||
'type': f'image/{ext}'
|
'type': mime_type
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -24,9 +24,12 @@ interface PostCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PostCard: React.FC<PostCardProps> = ({ post, onClick }) => {
|
const PostCard: React.FC<PostCardProps> = ({ post, onClick }) => {
|
||||||
// 格式化时间
|
// 格式化时间 - 将 UTC 时间转换为北京时间
|
||||||
const formatTime = (dateStr: string) => {
|
const formatTime = (dateStr: string) => {
|
||||||
return formatDistanceToNow(new Date(dateStr), {
|
const date = new Date(dateStr);
|
||||||
|
// 后端存储的是 UTC 时间,需要加 8 小时转换为北京时间
|
||||||
|
const beijingDate = new Date(date.getTime() + 8 * 60 * 60 * 1000);
|
||||||
|
return formatDistanceToNow(beijingDate, {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
locale: zhCN,
|
locale: zhCN,
|
||||||
});
|
});
|
||||||
@@ -87,7 +90,10 @@ const PostCard: React.FC<PostCardProps> = ({ post, onClick }) => {
|
|||||||
noOfLines={2}
|
noOfLines={2}
|
||||||
mb={2}
|
mb={2}
|
||||||
>
|
>
|
||||||
{post.content.replace(/<[^>]*>/g, '').slice(0, 150)}
|
{post.content
|
||||||
|
.replace(/<[^>]*>/g, '') // 移除 HTML 标签
|
||||||
|
.replace(/!\[[^\]]*\]\([^)]+\)/g, '[图片]') // 将 Markdown 图片替换为 [图片]
|
||||||
|
.slice(0, 150)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* 标签 */}
|
{/* 标签 */}
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// 颜色
|
// 深色主题颜色(HeroUI 风格)
|
||||||
const bgColor = useColorModeValue('white', 'gray.800');
|
const bgColor = 'rgba(17, 24, 39, 0.95)';
|
||||||
const headerBg = useColorModeValue('gray.50', 'gray.900');
|
const headerBg = 'rgba(17, 24, 39, 0.98)';
|
||||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
const borderColor = 'rgba(255, 255, 255, 0.1)';
|
||||||
const textColor = useColorModeValue('gray.800', 'gray.100');
|
const textColor = 'gray.100';
|
||||||
const mutedColor = useColorModeValue('gray.500', 'gray.400');
|
const mutedColor = 'gray.400';
|
||||||
|
|
||||||
// 加载回复
|
// 加载回复
|
||||||
const loadReplies = useCallback(async (pageNum: number = 1, append: boolean = false) => {
|
const loadReplies = useCallback(async (pageNum: number = 1, append: boolean = false) => {
|
||||||
@@ -156,9 +156,12 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间 - 将 UTC 时间转换为北京时间
|
||||||
const formatTime = (dateStr: string) => {
|
const formatTime = (dateStr: string) => {
|
||||||
return format(new Date(dateStr), 'yyyy年M月d日 HH:mm', { locale: zhCN });
|
const date = new Date(dateStr);
|
||||||
|
// 后端存储的是 UTC 时间,需要加 8 小时转换为北京时间
|
||||||
|
const beijingDate = new Date(date.getTime() + 8 * 60 * 60 * 1000);
|
||||||
|
return format(beijingDate, 'yyyy年M月d日 HH:mm', { locale: zhCN });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -177,9 +180,11 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
icon={<MdArrowBack />}
|
icon={<MdArrowBack />}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
mr={2}
|
mr={2}
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
/>
|
/>
|
||||||
<Text fontWeight="bold" flex={1} isTruncated>
|
<Text fontWeight="bold" flex={1} isTruncated color="white">
|
||||||
{post.title}
|
{post.title}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -189,7 +194,7 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
{/* 帖子内容 */}
|
{/* 帖子内容 */}
|
||||||
<Box p={6} bg={bgColor}>
|
<Box p={6} bg={bgColor}>
|
||||||
{/* 标题 */}
|
{/* 标题 */}
|
||||||
<Text fontSize="2xl" fontWeight="bold" mb={4}>
|
<Text fontSize="2xl" fontWeight="bold" mb={4} color="white">
|
||||||
{post.title}
|
{post.title}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -200,9 +205,10 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
name={post.authorName}
|
name={post.authorName}
|
||||||
src={post.authorAvatar}
|
src={post.authorAvatar}
|
||||||
mr={3}
|
mr={3}
|
||||||
|
bg="linear-gradient(135deg, rgba(139, 92, 246, 0.6), rgba(59, 130, 246, 0.6))"
|
||||||
/>
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fontWeight="semibold">{post.authorName}</Text>
|
<Text fontWeight="semibold" color="purple.300">{post.authorName}</Text>
|
||||||
<Text fontSize="sm" color={mutedColor}>
|
<Text fontSize="sm" color={mutedColor}>
|
||||||
发布于 {formatTime(post.createdAt)}
|
发布于 {formatTime(post.createdAt)}
|
||||||
{post.updatedAt && post.updatedAt !== post.createdAt && (
|
{post.updatedAt && post.updatedAt !== post.createdAt && (
|
||||||
@@ -216,7 +222,14 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
{post.tags && post.tags.length > 0 && (
|
{post.tags && post.tags.length > 0 && (
|
||||||
<HStack spacing={2} mb={4} flexWrap="wrap">
|
<HStack spacing={2} mb={4} flexWrap="wrap">
|
||||||
{post.tags.map(tag => (
|
{post.tags.map(tag => (
|
||||||
<Tag key={tag} colorScheme="blue" size="sm">
|
<Tag
|
||||||
|
key={tag}
|
||||||
|
size="sm"
|
||||||
|
bg="rgba(59, 130, 246, 0.15)"
|
||||||
|
color="blue.300"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(59, 130, 246, 0.3)"
|
||||||
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
@@ -229,27 +242,42 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
fontSize="md"
|
fontSize="md"
|
||||||
lineHeight="1.8"
|
lineHeight="1.8"
|
||||||
color={textColor}
|
color={textColor}
|
||||||
dangerouslySetInnerHTML={{ __html: post.contentHtml || post.content }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: (() => {
|
||||||
|
// 将 Markdown 图片语法转换为 HTML img 标签
|
||||||
|
let html = post.contentHtml || post.content;
|
||||||
|
// 匹配  格式,支持 base64 data URL
|
||||||
|
html = html.replace(
|
||||||
|
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
||||||
|
'<img src="$2" alt="$1" style="max-width: 100%; border-radius: 8px; margin: 16px 0;" />'
|
||||||
|
);
|
||||||
|
// 将换行转换为 <br>
|
||||||
|
html = html.replace(/\n/g, '<br />');
|
||||||
|
return html;
|
||||||
|
})()
|
||||||
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
'p': { mb: 4 },
|
'p': { mb: 4 },
|
||||||
'img': { maxW: '100%', borderRadius: 'md', my: 4 },
|
'img': { maxW: '100%', borderRadius: 'md', my: 4 },
|
||||||
'a': { color: 'blue.500', textDecoration: 'underline' },
|
'a': { color: 'blue.400', textDecoration: 'underline' },
|
||||||
'blockquote': {
|
'blockquote': {
|
||||||
borderLeftWidth: '4px',
|
borderLeftWidth: '4px',
|
||||||
borderLeftColor: 'blue.400',
|
borderLeftColor: 'purple.400',
|
||||||
pl: 4,
|
pl: 4,
|
||||||
py: 2,
|
py: 2,
|
||||||
my: 4,
|
my: 4,
|
||||||
color: mutedColor,
|
color: mutedColor,
|
||||||
|
bg: 'rgba(255, 255, 255, 0.03)',
|
||||||
},
|
},
|
||||||
'code': {
|
'code': {
|
||||||
bg: 'gray.100',
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'purple.300',
|
||||||
px: 1,
|
px: 1,
|
||||||
borderRadius: 'sm',
|
borderRadius: 'sm',
|
||||||
fontFamily: 'mono',
|
fontFamily: 'mono',
|
||||||
},
|
},
|
||||||
'pre': {
|
'pre': {
|
||||||
bg: 'gray.100',
|
bg: 'rgba(0, 0, 0, 0.3)',
|
||||||
p: 4,
|
p: 4,
|
||||||
borderRadius: 'md',
|
borderRadius: 'md',
|
||||||
overflowX: 'auto',
|
overflowX: 'auto',
|
||||||
@@ -272,15 +300,29 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
leftIcon={<MdThumbUp />}
|
leftIcon={<MdThumbUp />}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={liked ? 'solid' : 'ghost'}
|
variant={liked ? 'solid' : 'ghost'}
|
||||||
colorScheme={liked ? 'blue' : 'gray'}
|
colorScheme={liked ? 'purple' : 'gray'}
|
||||||
|
color={liked ? 'white' : 'gray.400'}
|
||||||
|
_hover={{ bg: 'whiteAlpha.100' }}
|
||||||
onClick={handleLike}
|
onClick={handleLike}
|
||||||
>
|
>
|
||||||
{likeCount}
|
{likeCount}
|
||||||
</Button>
|
</Button>
|
||||||
<Button leftIcon={<MdBookmark />} size="sm" variant="ghost">
|
<Button
|
||||||
|
leftIcon={<MdBookmark />}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||||
|
>
|
||||||
收藏
|
收藏
|
||||||
</Button>
|
</Button>
|
||||||
<Button leftIcon={<MdShare />} size="sm" variant="ghost">
|
<Button
|
||||||
|
leftIcon={<MdShare />}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||||
|
>
|
||||||
分享
|
分享
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -290,6 +332,8 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
icon={<MdMoreVert />}
|
icon={<MdMoreVert />}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -298,20 +342,20 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
|
|
||||||
{/* 回复区域 */}
|
{/* 回复区域 */}
|
||||||
<Box p={4} bg={bgColor}>
|
<Box p={4} bg={bgColor}>
|
||||||
<Text fontWeight="bold" mb={4}>
|
<Text fontWeight="bold" mb={4} color="white">
|
||||||
全部回复 ({post.replyCount})
|
全部回复 ({post.replyCount})
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Flex justify="center" py={8}>
|
<Flex justify="center" py={8}>
|
||||||
<Spinner />
|
<Spinner color="purple.400" />
|
||||||
</Flex>
|
</Flex>
|
||||||
) : replies.length === 0 ? (
|
) : replies.length === 0 ? (
|
||||||
<Flex justify="center" py={8}>
|
<Flex justify="center" py={8}>
|
||||||
<Text color={mutedColor}>暂无回复,快来抢沙发!</Text>
|
<Text color={mutedColor}>暂无回复,快来抢沙发!</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
<VStack spacing={0} align="stretch" divider={<Divider />}>
|
<VStack spacing={0} align="stretch" divider={<Divider borderColor={borderColor} />}>
|
||||||
{replies.map(reply => (
|
{replies.map(reply => (
|
||||||
<ReplyItem
|
<ReplyItem
|
||||||
key={reply.id}
|
key={reply.id}
|
||||||
@@ -328,6 +372,8 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||||
isLoading={loadingMore}
|
isLoading={loadingMore}
|
||||||
onClick={() => loadReplies(page + 1, true)}
|
onClick={() => loadReplies(page + 1, true)}
|
||||||
>
|
>
|
||||||
@@ -347,14 +393,21 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
align="center"
|
align="center"
|
||||||
mb={2}
|
mb={2}
|
||||||
p={2}
|
p={2}
|
||||||
bg={bgColor}
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
|
color="gray.300"
|
||||||
>
|
>
|
||||||
<Text flex={1} isTruncated>
|
<Text flex={1} isTruncated>
|
||||||
回复 <strong>{replyTo.authorName}</strong>: {replyTo.content.slice(0, 50)}
|
回复 <strong>{replyTo.authorName}</strong>: {replyTo.content.slice(0, 50)}
|
||||||
</Text>
|
</Text>
|
||||||
<Button size="xs" variant="ghost" onClick={() => setReplyTo(null)}>
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100' }}
|
||||||
|
onClick={() => setReplyTo(null)}
|
||||||
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -368,11 +421,17 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
resize="none"
|
resize="none"
|
||||||
rows={2}
|
rows={2}
|
||||||
mr={2}
|
mr={2}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
color="white"
|
||||||
|
_placeholder={{ color: 'gray.500' }}
|
||||||
|
_focus={{ borderColor: 'purple.400', boxShadow: '0 0 0 1px var(--chakra-colors-purple-400)' }}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="发送"
|
aria-label="发送"
|
||||||
icon={<MdSend />}
|
icon={<MdSend />}
|
||||||
colorScheme="blue"
|
colorScheme="purple"
|
||||||
isLoading={submitting}
|
isLoading={submitting}
|
||||||
isDisabled={!replyContent.trim()}
|
isDisabled={!replyContent.trim()}
|
||||||
onClick={handleSubmitReply}
|
onClick={handleSubmitReply}
|
||||||
@@ -388,7 +447,7 @@ const PostDetail: React.FC<PostDetailProps> = ({ post, onBack }) => {
|
|||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
bg={headerBg}
|
bg={headerBg}
|
||||||
>
|
>
|
||||||
<MdLock />
|
<MdLock color="gray" />
|
||||||
<Text ml={2} color={mutedColor}>
|
<Text ml={2} color={mutedColor}>
|
||||||
该帖子已锁定,无法回复
|
该帖子已锁定,无法回复
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -110,11 +110,27 @@ export const getMessages = async (
|
|||||||
): Promise<PaginatedResponse<Message>> => {
|
): Promise<PaginatedResponse<Message>> => {
|
||||||
const { before, after, limit = 50 } = options;
|
const { before, after, limit = 50 } = options;
|
||||||
|
|
||||||
// 构建 ES 查询 - 使用最简单的 match_all 先测试
|
// 构建 ES 查询
|
||||||
|
const mustClauses: any[] = [
|
||||||
|
{ match: { channel_id: channelId } },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 分页游标 - 加载更早的消息
|
||||||
|
if (before) {
|
||||||
|
mustClauses.push({ range: { created_at: { lt: before } } });
|
||||||
|
}
|
||||||
|
// 加载更新的消息
|
||||||
|
if (after) {
|
||||||
|
mustClauses.push({ range: { created_at: { gt: after } } });
|
||||||
|
}
|
||||||
|
|
||||||
const esBody: any = {
|
const esBody: any = {
|
||||||
query: {
|
query: {
|
||||||
match: {
|
bool: {
|
||||||
channel_id: channelId,
|
must: mustClauses,
|
||||||
|
must_not: [
|
||||||
|
{ match: { is_deleted: true } },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sort: [
|
sort: [
|
||||||
@@ -123,22 +139,6 @@ export const getMessages = async (
|
|||||||
size: limit,
|
size: limit,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 分页游标
|
|
||||||
if (before) {
|
|
||||||
esBody.query = {
|
|
||||||
bool: {
|
|
||||||
must: [
|
|
||||||
{ match: { channel_id: channelId } },
|
|
||||||
],
|
|
||||||
filter: [
|
|
||||||
{ range: { created_at: { lt: before } } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[getMessages] ES 查询:', JSON.stringify(esBody, null, 2));
|
|
||||||
|
|
||||||
const response = await fetch(`${ES_API_BASE}/community_messages/_search`, {
|
const response = await fetch(`${ES_API_BASE}/community_messages/_search`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
Reference in New Issue
Block a user