个股论坛重做
This commit is contained in:
@@ -14,7 +14,6 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Divider,
|
Divider,
|
||||||
Textarea,
|
Textarea,
|
||||||
useColorModeValue,
|
|
||||||
Spinner,
|
Spinner,
|
||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
IconButton,
|
IconButton,
|
||||||
HStack,
|
HStack,
|
||||||
useColorModeValue,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { MdThumbUp, MdReply, MdMoreVert } from 'react-icons/md';
|
import { MdThumbUp, MdReply, MdMoreVert } from 'react-icons/md';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
@@ -26,18 +25,35 @@ const ReplyItem: React.FC<ReplyItemProps> = ({ reply, onReply }) => {
|
|||||||
const [liked, setLiked] = useState(false);
|
const [liked, setLiked] = useState(false);
|
||||||
const [likeCount, setLikeCount] = useState(reply.likeCount);
|
const [likeCount, setLikeCount] = useState(reply.likeCount);
|
||||||
|
|
||||||
const textColor = useColorModeValue('gray.800', 'gray.100');
|
// 深色主题颜色(HeroUI 风格)
|
||||||
const mutedColor = useColorModeValue('gray.500', 'gray.400');
|
const textColor = 'gray.100';
|
||||||
const replyBg = useColorModeValue('gray.100', 'gray.700');
|
const mutedColor = 'gray.400';
|
||||||
|
const replyBg = 'rgba(255, 255, 255, 0.05)';
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间 - 将 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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 解析内容中的 Markdown 图片
|
||||||
|
const parseContent = (content: string) => {
|
||||||
|
let html = content;
|
||||||
|
// 匹配  格式,支持 base64 data URL
|
||||||
|
html = html.replace(
|
||||||
|
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
||||||
|
'<img src="$2" alt="$1" style="max-width: 100%; border-radius: 8px; margin: 8px 0;" />'
|
||||||
|
);
|
||||||
|
// 将换行转换为 <br>
|
||||||
|
html = html.replace(/\n/g, '<br />');
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
// 点赞
|
// 点赞
|
||||||
const handleLike = () => {
|
const handleLike = () => {
|
||||||
setLiked(!liked);
|
setLiked(!liked);
|
||||||
@@ -53,13 +69,14 @@ const ReplyItem: React.FC<ReplyItemProps> = ({ reply, onReply }) => {
|
|||||||
name={reply.authorName}
|
name={reply.authorName}
|
||||||
src={reply.authorAvatar}
|
src={reply.authorAvatar}
|
||||||
mr={3}
|
mr={3}
|
||||||
|
bg="linear-gradient(135deg, rgba(139, 92, 246, 0.6), rgba(59, 130, 246, 0.6))"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 内容 */}
|
{/* 内容 */}
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
{/* 作者和时间 */}
|
{/* 作者和时间 */}
|
||||||
<HStack spacing={2} mb={1}>
|
<HStack spacing={2} mb={1}>
|
||||||
<Text fontWeight="semibold" fontSize="sm">
|
<Text fontWeight="semibold" fontSize="sm" color="purple.300">
|
||||||
{reply.authorName}
|
{reply.authorName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color={mutedColor}>
|
<Text fontSize="xs" color={mutedColor}>
|
||||||
@@ -68,9 +85,9 @@ const ReplyItem: React.FC<ReplyItemProps> = ({ reply, onReply }) => {
|
|||||||
{reply.isSolution && (
|
{reply.isSolution && (
|
||||||
<Text
|
<Text
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
color="green.500"
|
color="green.400"
|
||||||
fontWeight="semibold"
|
fontWeight="semibold"
|
||||||
bg="green.50"
|
bg="rgba(34, 197, 94, 0.15)"
|
||||||
px={2}
|
px={2}
|
||||||
py={0.5}
|
py={0.5}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
@@ -96,11 +113,14 @@ const ReplyItem: React.FC<ReplyItemProps> = ({ reply, onReply }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 回复内容 */}
|
{/* 回复内容 */}
|
||||||
<Text
|
<Box
|
||||||
color={textColor}
|
color={textColor}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
lineHeight="1.6"
|
lineHeight="1.6"
|
||||||
dangerouslySetInnerHTML={{ __html: reply.contentHtml || reply.content }}
|
dangerouslySetInnerHTML={{ __html: parseContent(reply.contentHtml || reply.content) }}
|
||||||
|
sx={{
|
||||||
|
'img': { maxW: '100%', borderRadius: 'md', my: 2 },
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 操作栏 */}
|
{/* 操作栏 */}
|
||||||
@@ -110,7 +130,9 @@ const ReplyItem: React.FC<ReplyItemProps> = ({ reply, onReply }) => {
|
|||||||
icon={<MdThumbUp />}
|
icon={<MdThumbUp />}
|
||||||
size="xs"
|
size="xs"
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
<Text fontSize="xs" color={mutedColor}>
|
<Text fontSize="xs" color={mutedColor}>
|
||||||
@@ -122,6 +144,8 @@ const ReplyItem: React.FC<ReplyItemProps> = ({ reply, onReply }) => {
|
|||||||
icon={<MdReply />}
|
icon={<MdReply />}
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||||
onClick={onReply}
|
onClick={onReply}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -130,6 +154,8 @@ const ReplyItem: React.FC<ReplyItemProps> = ({ reply, onReply }) => {
|
|||||||
icon={<MdMoreVert />}
|
icon={<MdMoreVert />}
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{ bg: 'whiteAlpha.100', color: 'white' }}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user