个股论坛重做

This commit is contained in:
2026-01-06 14:24:01 +08:00
parent 961d6482c2
commit 2359726be9
2 changed files with 670 additions and 156 deletions

View File

@@ -340,8 +340,9 @@ def get_channel_admins(channel_id):
with get_db_engine().connect() as conn:
# 1. 先获取全局超级管理员
# 注意users 表可能没有 avatar 字段,使用 NULL 作为默认值
super_admin_sql = text("""
SELECT ca.user_id, u.username, u.avatar
SELECT ca.user_id, u.username
FROM community_admins ca
LEFT JOIN users u ON ca.user_id = u.id
WHERE ca.role = 'admin'
@@ -351,8 +352,8 @@ def get_channel_admins(channel_id):
for row in super_admins:
admins.append({
'userId': str(row.user_id),
'username': row.username,
'avatar': row.avatar,
'username': row.username or f'用户{row.user_id}',
'avatar': None,
'role': 'super_admin', # 特殊标记
'isSuperAdmin': True,
'permissions': {
@@ -370,44 +371,51 @@ def get_channel_admins(channel_id):
# 2. 获取频道管理员(排除已在超级管理员列表中的用户)
super_admin_ids = [str(row.user_id) for row in super_admins]
sql = text("""
SELECT cca.user_id, cca.role, cca.permissions, cca.created_at,
u.username, u.avatar
FROM community_channel_admins cca
LEFT JOIN users u ON cca.user_id = u.id
WHERE cca.channel_id = :channel_id
ORDER BY
CASE cca.role
WHEN 'owner' THEN 1
WHEN 'admin' THEN 2
WHEN 'moderator' THEN 3
END
""")
result = conn.execute(sql, {'channel_id': channel_id}).fetchall()
# 检查 community_channel_admins 表是否存在
try:
sql = text("""
SELECT cca.user_id, cca.role, cca.permissions, cca.created_at,
u.username
FROM community_channel_admins cca
LEFT JOIN users u ON cca.user_id = u.id
WHERE cca.channel_id = :channel_id
ORDER BY
CASE cca.role
WHEN 'owner' THEN 1
WHEN 'admin' THEN 2
WHEN 'moderator' THEN 3
END
""")
result = conn.execute(sql, {'channel_id': channel_id}).fetchall()
for row in result:
# 跳过已在超级管理员列表中的用户
if str(row.user_id) in super_admin_ids:
continue
for row in result:
# 跳过已在超级管理员列表中的用户
if str(row.user_id) in super_admin_ids:
continue
permissions = row.permissions
if isinstance(permissions, str):
permissions = json.loads(permissions)
permissions = row.permissions
if isinstance(permissions, str):
permissions = json.loads(permissions)
admins.append({
'userId': str(row.user_id),
'username': row.username,
'avatar': row.avatar,
'role': row.role,
'isSuperAdmin': False,
'permissions': permissions or {},
'createdAt': row.created_at.isoformat() if row.created_at else None
})
admins.append({
'userId': str(row.user_id),
'username': row.username or f'用户{row.user_id}',
'avatar': None,
'role': row.role,
'isSuperAdmin': False,
'permissions': permissions or {},
'createdAt': row.created_at.isoformat() if row.created_at else None
})
except Exception as table_err:
# 表不存在时忽略,只返回超级管理员
print(f"[Community API] 频道管理员表查询失败(可能表不存在): {table_err}")
return api_response(admins)
except Exception as e:
print(f"[Community API] 获取频道管理员列表失败: {e}")
import traceback
traceback.print_exc()
return api_error(f'获取管理员列表失败: {str(e)}', 500)

View File

@@ -1,7 +1,8 @@
/**
* 创建帖子弹窗
* 创建帖子弹窗 - 增强版
* 支持图片上传和股票关联
*/
import React, { useState } from 'react';
import React, { useState, useRef, useCallback } from 'react';
import {
Modal,
ModalOverlay,
@@ -17,18 +18,34 @@ import {
FormLabel,
FormHelperText,
HStack,
VStack,
Tag,
TagLabel,
TagCloseButton,
useColorModeValue,
useToast,
Box,
Wrap,
WrapItem,
Icon,
IconButton,
Image,
Spinner,
Text,
InputGroup,
InputLeftElement,
List,
ListItem,
Flex,
Badge,
Divider,
Tooltip,
Progress,
} from '@chakra-ui/react';
import { Image as ImageIcon, X, Search, TrendingUp, Plus, Trash2 } from 'lucide-react';
import { useDropzone } from 'react-dropzone';
import { ForumPost } from '../../../types';
import { createForumPost } from '../../../services/communityService';
import { createForumPost, uploadImage, searchStocks, StockSearchResult } from '../../../services/communityService';
interface CreatePostModalProps {
isOpen: boolean;
@@ -41,6 +58,20 @@ interface CreatePostModalProps {
// 预设标签
const PRESET_TAGS = ['分析', '策略', '新闻', '提问', '讨论', '复盘', '干货', '观点'];
// 上传的图片接口
interface UploadedImage {
url: string;
filename: string;
uploading?: boolean;
error?: string;
}
// 选中的股票
interface SelectedStock {
code: string;
name: string;
}
const CreatePostModal: React.FC<CreatePostModalProps> = ({
isOpen,
onClose,
@@ -54,22 +85,35 @@ const CreatePostModal: React.FC<CreatePostModalProps> = ({
const [customTag, setCustomTag] = useState('');
const [submitting, setSubmitting] = useState(false);
const toast = useToast();
const tagBg = useColorModeValue('gray.100', 'gray.700');
// 图片相关状态
const [images, setImages] = useState<UploadedImage[]>([]);
const [uploadingCount, setUploadingCount] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null);
// 股票搜索相关状态
const [stockQuery, setStockQuery] = useState('');
const [stockResults, setStockResults] = useState<StockSearchResult[]>([]);
const [searchingStocks, setSearchingStocks] = useState(false);
const [selectedStocks, setSelectedStocks] = useState<SelectedStock[]>([]);
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const toast = useToast();
const textareaRef = useRef<HTMLTextAreaElement>(null);
// ============================================================
// 标签相关
// ============================================================
// 添加标签
const addTag = (tag: string) => {
if (tag && !tags.includes(tag) && tags.length < 5) {
setTags([...tags, tag]);
}
};
// 移除标签
const removeTag = (tag: string) => {
setTags(tags.filter(t => t !== tag));
};
// 添加自定义标签
const handleAddCustomTag = () => {
if (customTag.trim()) {
addTag(customTag.trim());
@@ -77,47 +121,237 @@ const CreatePostModal: React.FC<CreatePostModalProps> = ({
}
};
// 提交帖子
const handleSubmit = async () => {
if (!title.trim()) {
// ============================================================
// 图片上传相关
// ============================================================
const handleUploadImage = useCallback(async (file: File) => {
// 检查文件类型
if (!file.type.startsWith('image/')) {
toast({
title: '请输入标题',
title: '只能上传图片文件',
status: 'warning',
duration: 2000,
});
return;
}
if (!content.trim()) {
// 检查文件大小10MB
if (file.size > 10 * 1024 * 1024) {
toast({
title: '请输入内容',
title: '图片大小不能超过 10MB',
status: 'warning',
duration: 2000,
});
return;
}
// 添加临时占位
const tempId = `temp_${Date.now()}`;
setImages(prev => [...prev, { url: '', filename: tempId, uploading: true }]);
setUploadingCount(prev => prev + 1);
try {
const result = await uploadImage(file);
// 替换临时占位为实际结果
setImages(prev =>
prev.map(img =>
img.filename === tempId
? { url: result.url, filename: result.filename, uploading: false }
: img
)
);
toast({
title: '图片上传成功',
status: 'success',
duration: 1500,
});
} catch (error: any) {
// 标记上传失败
setImages(prev =>
prev.map(img =>
img.filename === tempId
? { ...img, uploading: false, error: error.message }
: img
)
);
toast({
title: '图片上传失败',
description: error.message,
status: 'error',
duration: 3000,
});
} finally {
setUploadingCount(prev => prev - 1);
}
}, [toast]);
// 拖拽上传
const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: {
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'],
},
maxSize: 10 * 1024 * 1024,
onDrop: (acceptedFiles) => {
acceptedFiles.forEach(file => handleUploadImage(file));
},
noClick: true, // 禁用点击,使用自定义按钮
});
// 点击选择文件
const handleSelectFile = () => {
fileInputRef.current?.click();
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files) {
Array.from(files).forEach(file => handleUploadImage(file));
}
// 清空 input允许重复选择同一文件
e.target.value = '';
};
// 移除图片
const removeImage = (filename: string) => {
setImages(prev => prev.filter(img => img.filename !== filename));
};
// 插入图片到内容
const insertImageToContent = (url: string) => {
const imageMarkdown = `![图片](${url})\n`;
const textarea = textareaRef.current;
if (textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newContent = content.substring(0, start) + imageMarkdown + content.substring(end);
setContent(newContent);
// 移动光标到插入内容之后
setTimeout(() => {
textarea.selectionStart = textarea.selectionEnd = start + imageMarkdown.length;
textarea.focus();
}, 0);
} else {
setContent(prev => prev + imageMarkdown);
}
};
// ============================================================
// 股票搜索相关
// ============================================================
const handleStockSearch = useCallback(async (query: string) => {
if (!query.trim()) {
setStockResults([]);
return;
}
setSearchingStocks(true);
try {
const results = await searchStocks(query, 10, 'stock');
setStockResults(results);
} catch (error) {
console.error('搜索股票失败:', error);
} finally {
setSearchingStocks(false);
}
}, []);
// 防抖搜索
const handleStockQueryChange = (value: string) => {
setStockQuery(value);
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
searchTimeoutRef.current = setTimeout(() => {
handleStockSearch(value);
}, 300);
};
// 选择股票
const selectStock = (stock: StockSearchResult) => {
if (selectedStocks.length >= 5) {
toast({
title: '最多关联 5 只股票',
status: 'warning',
duration: 2000,
});
return;
}
if (selectedStocks.some(s => s.code === stock.stock_code)) {
toast({
title: '该股票已添加',
status: 'info',
duration: 1500,
});
return;
}
setSelectedStocks(prev => [...prev, { code: stock.stock_code, name: stock.stock_name }]);
setStockQuery('');
setStockResults([]);
};
// 移除股票
const removeStock = (code: string) => {
setSelectedStocks(prev => prev.filter(s => s.code !== code));
};
// ============================================================
// 提交相关
// ============================================================
const handleSubmit = async () => {
if (!title.trim()) {
toast({ title: '请输入标题', status: 'warning', duration: 2000 });
return;
}
if (!content.trim()) {
toast({ title: '请输入内容', status: 'warning', duration: 2000 });
return;
}
if (uploadingCount > 0) {
toast({ title: '请等待图片上传完成', status: 'warning', duration: 2000 });
return;
}
try {
setSubmitting(true);
// 构建内容:将图片 URL 转换为 Markdown 格式(如果用户没有手动插入)
let finalContent = content;
// 检查是否有未插入的图片,添加到末尾
const uploadedImages = images.filter(img => !img.error && !img.uploading && img.url);
const contentImageUrls = uploadedImages.map(img => img.url);
const missingImages = contentImageUrls.filter(url => !content.includes(url));
if (missingImages.length > 0) {
finalContent += '\n\n' + missingImages.map(url => `![图片](${url})`).join('\n');
}
const newPost = await createForumPost({
channelId,
title: title.trim(),
content: content.trim(),
content: finalContent.trim(),
tags,
stockSymbols: selectedStocks.map(s => s.code),
});
toast({
title: '发布成功',
status: 'success',
duration: 2000,
});
toast({ title: '发布成功', status: 'success', duration: 2000 });
// 重置表单
setTitle('');
setContent('');
setTags([]);
resetForm();
onPostCreated(newPost);
} catch (error) {
console.error('发布失败:', error);
@@ -132,128 +366,400 @@ const CreatePostModal: React.FC<CreatePostModalProps> = ({
}
};
// 关闭时重置
const handleClose = () => {
// 重置表单
const resetForm = () => {
setTitle('');
setContent('');
setTags([]);
setCustomTag('');
setImages([]);
setSelectedStocks([]);
setStockQuery('');
setStockResults([]);
};
const handleClose = () => {
resetForm();
onClose();
};
return (
<Modal isOpen={isOpen} onClose={handleClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Modal isOpen={isOpen} onClose={handleClose} size="2xl" scrollBehavior="inside">
<ModalOverlay bg="blackAlpha.700" backdropFilter="blur(8px)" />
<ModalContent
bg="rgba(17, 24, 39, 0.95)"
borderColor="rgba(255, 255, 255, 0.1)"
borderWidth="1px"
maxH="90vh"
>
<ModalHeader color="white" borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
#{channelName}
</ModalHeader>
<ModalCloseButton />
<ModalCloseButton color="gray.400" />
<ModalBody>
{/* 标题 */}
<FormControl mb={4} isRequired>
<FormLabel></FormLabel>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="输入帖子标题"
maxLength={100}
/>
<FormHelperText textAlign="right">
{title.length}/100
</FormHelperText>
</FormControl>
<ModalBody py={4} {...getRootProps()}>
<input {...getInputProps()} />
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/*"
multiple
style={{ display: 'none' }}
/>
{/* 内容 */}
<FormControl mb={4} isRequired>
<FormLabel></FormLabel>
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="分享你的观点..."
rows={8}
maxLength={10000}
/>
<FormHelperText textAlign="right">
{content.length}/10000
</FormHelperText>
</FormControl>
{/* 标签 */}
<FormControl mb={4}>
<FormLabel>5</FormLabel>
{/* 已选标签 */}
{tags.length > 0 && (
<HStack spacing={2} mb={2} flexWrap="wrap">
{tags.map(tag => (
<Tag
key={tag}
size="md"
colorScheme="blue"
borderRadius="full"
>
<TagLabel>{tag}</TagLabel>
<TagCloseButton onClick={() => removeTag(tag)} />
</Tag>
))}
</HStack>
)}
{/* 预设标签 */}
<Box mb={2}>
<Wrap spacing={2}>
{PRESET_TAGS.filter(t => !tags.includes(t)).map(tag => (
<WrapItem key={tag}>
<Tag
size="sm"
bg={tagBg}
cursor="pointer"
_hover={{ opacity: 0.8 }}
onClick={() => addTag(tag)}
>
+ {tag}
</Tag>
</WrapItem>
))}
</Wrap>
{/* 拖拽提示 */}
{isDragActive && (
<Box
position="absolute"
inset={0}
bg="rgba(139, 92, 246, 0.2)"
borderRadius="md"
display="flex"
alignItems="center"
justifyContent="center"
zIndex={10}
border="2px dashed"
borderColor="purple.400"
>
<VStack spacing={2}>
<Icon as={ImageIcon} boxSize={12} color="purple.400" />
<Text color="purple.300" fontWeight="semibold"></Text>
</VStack>
</Box>
)}
{/* 自定义标签 */}
{tags.length < 5 && (
<HStack>
<Input
size="sm"
value={customTag}
onChange={(e) => setCustomTag(e.target.value)}
placeholder="输入自定义标签"
maxLength={20}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddCustomTag();
}
}}
/>
<Button size="sm" onClick={handleAddCustomTag}>
<VStack spacing={4} align="stretch">
{/* 标题 */}
<FormControl isRequired>
<FormLabel color="gray.300"></FormLabel>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="输入帖子标题"
maxLength={100}
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)' }}
/>
<FormHelperText color="gray.500" textAlign="right">
{title.length}/100
</FormHelperText>
</FormControl>
{/* 内容 */}
<FormControl isRequired>
<FormLabel color="gray.300">
<HStack justify="space-between">
<Text></Text>
<Tooltip label="支持 Markdown 格式">
<Badge colorScheme="purple" variant="subtle">Markdown</Badge>
</Tooltip>
</HStack>
</FormLabel>
<Textarea
ref={textareaRef}
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="分享你的观点... 支持 Markdown 格式,可以拖拽图片到此处上传"
rows={10}
maxLength={10000}
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)' }}
/>
<Flex justify="space-between" mt={1}>
<Button
size="xs"
variant="ghost"
leftIcon={<ImageIcon size={14} />}
color="gray.400"
_hover={{ color: 'purple.400', bg: 'whiteAlpha.100' }}
onClick={handleSelectFile}
>
</Button>
</HStack>
<Text color="gray.500" fontSize="xs">
{content.length}/10000
</Text>
</Flex>
</FormControl>
{/* 已上传图片预览 */}
{images.length > 0 && (
<Box>
<Text color="gray.400" fontSize="sm" mb={2}>
</Text>
<Wrap spacing={2}>
{images.map((img, index) => (
<WrapItem key={img.filename}>
<Box
position="relative"
w="80px"
h="80px"
borderRadius="md"
overflow="hidden"
border="1px solid"
borderColor={img.error ? 'red.400' : 'rgba(255, 255, 255, 0.1)'}
>
{img.uploading ? (
<Flex
w="full"
h="full"
align="center"
justify="center"
bg="rgba(0, 0, 0, 0.5)"
>
<Spinner size="sm" color="purple.400" />
</Flex>
) : img.error ? (
<Flex
w="full"
h="full"
align="center"
justify="center"
bg="rgba(239, 68, 68, 0.1)"
flexDir="column"
>
<Icon as={X} color="red.400" boxSize={5} />
<Text fontSize="2xs" color="red.400"></Text>
</Flex>
) : (
<>
<Image
src={img.url}
alt={img.filename}
w="full"
h="full"
objectFit="cover"
cursor="pointer"
onClick={() => insertImageToContent(img.url)}
_hover={{ opacity: 0.8 }}
/>
<IconButton
aria-label="删除图片"
icon={<Trash2 size={12} />}
size="xs"
position="absolute"
top={1}
right={1}
bg="rgba(0, 0, 0, 0.6)"
color="white"
_hover={{ bg: 'red.500' }}
onClick={(e) => {
e.stopPropagation();
removeImage(img.filename);
}}
/>
</>
)}
</Box>
</WrapItem>
))}
</Wrap>
</Box>
)}
</FormControl>
<Divider borderColor="rgba(255, 255, 255, 0.1)" />
{/* 关联股票 */}
<FormControl>
<FormLabel color="gray.300">
<HStack>
<Icon as={TrendingUp} boxSize={4} />
<Text>5</Text>
</HStack>
</FormLabel>
{/* 已选股票 */}
{selectedStocks.length > 0 && (
<Wrap spacing={2} mb={3}>
{selectedStocks.map(stock => (
<WrapItem key={stock.code}>
<Tag
size="md"
colorScheme="orange"
borderRadius="full"
variant="subtle"
>
<TagLabel>${stock.code} {stock.name}</TagLabel>
<TagCloseButton onClick={() => removeStock(stock.code)} />
</Tag>
</WrapItem>
))}
</Wrap>
)}
{/* 搜索框 */}
{selectedStocks.length < 5 && (
<Box position="relative">
<InputGroup size="sm">
<InputLeftElement>
{searchingStocks ? (
<Spinner size="xs" color="purple.400" />
) : (
<Search size={14} color="gray" />
)}
</InputLeftElement>
<Input
value={stockQuery}
onChange={(e) => handleStockQueryChange(e.target.value)}
placeholder="输入股票代码或名称搜索..."
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' }}
/>
</InputGroup>
{/* 搜索结果下拉 */}
{stockResults.length > 0 && (
<List
position="absolute"
top="100%"
left={0}
right={0}
mt={1}
bg="rgba(17, 24, 39, 0.98)"
border="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
borderRadius="md"
maxH="200px"
overflowY="auto"
zIndex={20}
boxShadow="0 10px 40px rgba(0, 0, 0, 0.5)"
>
{stockResults.map(stock => (
<ListItem
key={stock.stock_code}
px={3}
py={2}
cursor="pointer"
_hover={{ bg: 'whiteAlpha.100' }}
onClick={() => selectStock(stock)}
>
<HStack justify="space-between">
<HStack spacing={2}>
<Text color="orange.300" fontWeight="semibold" fontSize="sm">
{stock.stock_code}
</Text>
<Text color="white" fontSize="sm">
{stock.stock_name}
</Text>
</HStack>
<Badge
size="sm"
colorScheme={stock.isIndex ? 'blue' : 'gray'}
variant="subtle"
>
{stock.security_type || (stock.isIndex ? '指数' : 'A股')}
</Badge>
</HStack>
</ListItem>
))}
</List>
)}
</Box>
)}
</FormControl>
<Divider borderColor="rgba(255, 255, 255, 0.1)" />
{/* 标签 */}
<FormControl>
<FormLabel color="gray.300">5</FormLabel>
{/* 已选标签 */}
{tags.length > 0 && (
<Wrap spacing={2} mb={2}>
{tags.map(tag => (
<WrapItem key={tag}>
<Tag size="md" colorScheme="blue" borderRadius="full">
<TagLabel>{tag}</TagLabel>
<TagCloseButton onClick={() => removeTag(tag)} />
</Tag>
</WrapItem>
))}
</Wrap>
)}
{/* 预设标签 */}
<Box mb={2}>
<Wrap spacing={2}>
{PRESET_TAGS.filter(t => !tags.includes(t)).map(tag => (
<WrapItem key={tag}>
<Tag
size="sm"
bg="rgba(255, 255, 255, 0.05)"
color="gray.400"
cursor="pointer"
_hover={{ bg: 'whiteAlpha.200', color: 'white' }}
onClick={() => addTag(tag)}
>
<Plus size={12} style={{ marginRight: 4 }} />
{tag}
</Tag>
</WrapItem>
))}
</Wrap>
</Box>
{/* 自定义标签 */}
{tags.length < 5 && (
<HStack>
<Input
size="sm"
value={customTag}
onChange={(e) => setCustomTag(e.target.value)}
placeholder="输入自定义标签"
maxLength={20}
bg="rgba(255, 255, 255, 0.05)"
border="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
color="white"
_placeholder={{ color: 'gray.500' }}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddCustomTag();
}
}}
/>
<Button
size="sm"
colorScheme="purple"
variant="ghost"
onClick={handleAddCustomTag}
>
</Button>
</HStack>
)}
</FormControl>
</VStack>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={handleClose}>
<ModalFooter borderTop="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
<Button variant="ghost" color="gray.400" mr={3} onClick={handleClose}>
</Button>
<Button
colorScheme="blue"
colorScheme="purple"
isLoading={submitting}
isDisabled={uploadingCount > 0}
onClick={handleSubmit}
leftIcon={uploadingCount > 0 ? <Spinner size="xs" /> : undefined}
>
{uploadingCount > 0 ? `上传中 (${uploadingCount})` : '发布'}
</Button>
</ModalFooter>
</ModalContent>