Merge branch 'feature_bugfix/251201_py_h5_ui' of https://git.valuefrontier.cn/vf/vf_react into feature_bugfix/251201_py_h5_ui
This commit is contained in:
3
app.py
3
app.py
@@ -4763,6 +4763,8 @@ def login_with_wechat():
|
|||||||
'username': user.username,
|
'username': user.username,
|
||||||
'nickname': user.nickname or user.username,
|
'nickname': user.nickname or user.username,
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
|
'phone': user.phone,
|
||||||
|
'phone_confirmed': bool(user.phone_confirmed),
|
||||||
'avatar_url': user.avatar_url,
|
'avatar_url': user.avatar_url,
|
||||||
'has_wechat': True,
|
'has_wechat': True,
|
||||||
'wechat_open_id': user.wechat_open_id,
|
'wechat_open_id': user.wechat_open_id,
|
||||||
@@ -10958,6 +10960,7 @@ def create_event_post(event_id):
|
|||||||
'created_at': post.created_at.isoformat(),
|
'created_at': post.created_at.isoformat(),
|
||||||
'user': {
|
'user': {
|
||||||
'id': current_user.id,
|
'id': current_user.id,
|
||||||
|
'nickname': current_user.nickname, # 添加昵称,与导航区保持一致
|
||||||
'username': current_user.username,
|
'username': current_user.username,
|
||||||
'avatar_url': current_user.avatar_url
|
'avatar_url': current_user.avatar_url
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,63 @@
|
|||||||
// src/components/EventCommentSection/CommentItem.js
|
// src/components/EventCommentSection/CommentItem.js
|
||||||
/**
|
/**
|
||||||
* 单条评论组件
|
* 单条评论组件
|
||||||
* 功能:显示用户头像、昵称、时间、评论内容
|
* 功能:显示用户头像、昵称、时间、评论内容、删除按钮
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
HStack,
|
HStack,
|
||||||
VStack,
|
VStack,
|
||||||
Avatar,
|
Avatar,
|
||||||
Text,
|
Text,
|
||||||
|
IconButton,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
|
Tooltip,
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
Button,
|
||||||
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
import { DeleteIcon } from '@chakra-ui/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
const CommentItem = ({ comment }) => {
|
const CommentItem = ({ comment, currentUserId, onDelete }) => {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const cancelRef = React.useRef();
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const itemBg = useColorModeValue('gray.50', 'gray.700');
|
const itemBg = useColorModeValue('gray.50', 'gray.700');
|
||||||
const usernameColor = useColorModeValue('gray.800', 'gray.100');
|
const usernameColor = useColorModeValue('gray.800', 'gray.100');
|
||||||
const timeColor = useColorModeValue('gray.500', 'gray.400');
|
const timeColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
const contentColor = useColorModeValue('gray.700', 'gray.300');
|
const contentColor = useColorModeValue('gray.700', 'gray.300');
|
||||||
|
|
||||||
|
// 判断当前用户是否可以删除此评论
|
||||||
|
const canDelete = currentUserId && (
|
||||||
|
comment.author?.id === currentUserId ||
|
||||||
|
comment.user_id === currentUserId
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理删除
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!onDelete) return;
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await onDelete(comment.id);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
// 错误由父组件处理
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = (timestamp) => {
|
const formatTime = (timestamp) => {
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
@@ -46,43 +80,99 @@ const CommentItem = ({ comment }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<>
|
||||||
p={3}
|
<Box
|
||||||
bg={itemBg}
|
p={3}
|
||||||
borderRadius="md"
|
bg={itemBg}
|
||||||
transition="all 0.2s"
|
borderRadius="md"
|
||||||
_hover={{
|
transition="all 0.2s"
|
||||||
transform: 'translateY(-2px)',
|
_hover={{
|
||||||
boxShadow: 'sm',
|
transform: 'translateY(-2px)',
|
||||||
}}
|
boxShadow: 'sm',
|
||||||
>
|
}}
|
||||||
<HStack align="start" spacing={3}>
|
position="relative"
|
||||||
{/* 用户头像 */}
|
>
|
||||||
<Avatar
|
<HStack align="start" spacing={3}>
|
||||||
size="sm"
|
{/* 用户头像 */}
|
||||||
name={comment.author?.username || 'Anonymous'}
|
<Avatar
|
||||||
src={comment.author?.avatar}
|
size="sm"
|
||||||
/>
|
name={comment.author?.username || 'Anonymous'}
|
||||||
|
src={comment.author?.avatar}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 评论内容区 */}
|
{/* 评论内容区 */}
|
||||||
<VStack align="stretch" flex={1} spacing={1}>
|
<VStack align="stretch" flex={1} spacing={1}>
|
||||||
{/* 用户名和时间 */}
|
{/* 用户名和时间 */}
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2} justify="space-between">
|
||||||
<Text fontSize="sm" fontWeight="bold" color={usernameColor}>
|
<HStack spacing={2}>
|
||||||
{comment.author?.username || 'Anonymous'}
|
<Text fontSize="sm" fontWeight="bold" color={usernameColor}>
|
||||||
</Text>
|
{comment.author?.username || 'Anonymous'}
|
||||||
<Text fontSize="xs" color={timeColor}>
|
</Text>
|
||||||
{formatTime(comment.created_at)}
|
<Text fontSize="xs" color={timeColor}>
|
||||||
</Text>
|
{formatTime(comment.created_at)}
|
||||||
</HStack>
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
{/* 评论内容 */}
|
{/* 删除按钮 - 只对自己的评论显示 */}
|
||||||
<Text fontSize="sm" color={contentColor} lineHeight="1.6">
|
{canDelete && (
|
||||||
{comment.content}
|
<Tooltip label="删除评论" placement="top">
|
||||||
</Text>
|
<IconButton
|
||||||
</VStack>
|
icon={<DeleteIcon />}
|
||||||
</HStack>
|
size="xs"
|
||||||
</Box>
|
variant="ghost"
|
||||||
|
colorScheme="red"
|
||||||
|
aria-label="删除评论"
|
||||||
|
onClick={onOpen}
|
||||||
|
opacity={0.6}
|
||||||
|
_hover={{ opacity: 1 }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 评论内容 */}
|
||||||
|
<Text fontSize="sm" color={contentColor} lineHeight="1.6">
|
||||||
|
{comment.content}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<AlertDialog
|
||||||
|
isOpen={isOpen}
|
||||||
|
leastDestructiveRef={cancelRef}
|
||||||
|
onClose={onClose}
|
||||||
|
isCentered
|
||||||
|
>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
删除评论
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<AlertDialogBody>
|
||||||
|
确定要删除这条评论吗?此操作不可撤销。
|
||||||
|
</AlertDialogBody>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button ref={cancelRef} onClick={onClose} isDisabled={isDeleting}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorScheme="red"
|
||||||
|
onClick={handleDelete}
|
||||||
|
ml={3}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
loadingText="删除中..."
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { ChatIcon } from '@chakra-ui/icons';
|
import { ChatIcon } from '@chakra-ui/icons';
|
||||||
import CommentItem from './CommentItem';
|
import CommentItem from './CommentItem';
|
||||||
|
|
||||||
const CommentList = ({ comments, loading }) => {
|
const CommentList = ({ comments, loading, currentUserId, onDelete }) => {
|
||||||
const emptyTextColor = useColorModeValue('gray.500', 'gray.400');
|
const emptyTextColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
const emptyBgColor = useColorModeValue('gray.50', 'gray.700');
|
const emptyBgColor = useColorModeValue('gray.50', 'gray.700');
|
||||||
|
|
||||||
@@ -58,7 +58,12 @@ const CommentList = ({ comments, loading }) => {
|
|||||||
return (
|
return (
|
||||||
<VStack align="stretch" spacing={3}>
|
<VStack align="stretch" spacing={3}>
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
<CommentItem key={comment.id} comment={comment} />
|
<CommentItem
|
||||||
|
key={comment.id}
|
||||||
|
comment={comment}
|
||||||
|
currentUserId={currentUserId}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -144,8 +144,9 @@ const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) =>
|
|||||||
content_type: 'text',
|
content_type: 'text',
|
||||||
author: {
|
author: {
|
||||||
id: user?.id || 'current_user',
|
id: user?.id || 'current_user',
|
||||||
username: user?.username || '当前用户',
|
// 与导航区保持一致:优先显示昵称
|
||||||
avatar: user?.avatar || null,
|
username: user?.nickname || user?.username || user?.email || '当前用户',
|
||||||
|
avatar: user?.avatar_url || null,
|
||||||
},
|
},
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
likes_count: 0,
|
likes_count: 0,
|
||||||
@@ -187,6 +188,51 @@ const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) =>
|
|||||||
}
|
}
|
||||||
}, [eventId, commentText, toast, user, setComments, setTotalCount]);
|
}, [eventId, commentText, toast, user, setComments, setTotalCount]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除评论
|
||||||
|
*/
|
||||||
|
const handleDeleteComment = useCallback(async (commentId: string | number) => {
|
||||||
|
try {
|
||||||
|
const result = await eventService.deletePost(commentId);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 从本地 state 中移除该评论
|
||||||
|
setComments((prevComments) =>
|
||||||
|
prevComments.filter((comment) => comment.id !== commentId)
|
||||||
|
);
|
||||||
|
// 总评论数 -1
|
||||||
|
setTotalCount((prevTotal) => Math.max(0, prevTotal - 1));
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: '评论已删除',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('EventCommentSection', '评论删除成功', {
|
||||||
|
eventId,
|
||||||
|
commentId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '删除评论失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('EventCommentSection', 'handleDeleteComment', error, {
|
||||||
|
eventId,
|
||||||
|
commentId,
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
title: '删除评论失败',
|
||||||
|
description: error.message || '请稍后重试',
|
||||||
|
status: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
throw error; // 重新抛出让 CommentItem 知道删除失败
|
||||||
|
}
|
||||||
|
}, [eventId, toast, setComments, setTotalCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* 标题栏 */}
|
{/* 标题栏 */}
|
||||||
@@ -203,7 +249,12 @@ const EventCommentSection: React.FC<EventCommentSectionProps> = ({ eventId }) =>
|
|||||||
|
|
||||||
{/* 评论列表 */}
|
{/* 评论列表 */}
|
||||||
<Box mb={4}>
|
<Box mb={4}>
|
||||||
<CommentList comments={comments} loading={loading} />
|
<CommentList
|
||||||
|
comments={comments}
|
||||||
|
loading={loading}
|
||||||
|
currentUserId={user?.id}
|
||||||
|
onDelete={handleDeleteComment}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 加载更多按钮(仅当有更多评论时显示) */}
|
{/* 加载更多按钮(仅当有更多评论时显示) */}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const MobileDrawer = memo(({
|
|||||||
if (user.nickname) return user.nickname;
|
if (user.nickname) return user.nickname;
|
||||||
if (user.username) return user.username;
|
if (user.username) return user.username;
|
||||||
if (user.email) return user.email.split('@')[0];
|
if (user.email) return user.email.split('@')[0];
|
||||||
if (user.phone) return user.phone;
|
if (typeof user.phone === 'string' && user.phone) return user.phone;
|
||||||
return '用户';
|
return '用户';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ const MobileDrawer = memo(({
|
|||||||
/>
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||||
{user.phone && (
|
{typeof user.phone === 'string' && user.phone && (
|
||||||
<Text fontSize="xs" color={emailTextColor}>{user.phone}</Text>
|
<Text fontSize="xs" color={emailTextColor}>{user.phone}</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const PersonalCenterMenu = memo(({ user, handleLogout }) => {
|
|||||||
if (user.nickname) return user.nickname;
|
if (user.nickname) return user.nickname;
|
||||||
if (user.username) return user.username;
|
if (user.username) return user.username;
|
||||||
if (user.email) return user.email.split('@')[0];
|
if (user.email) return user.email.split('@')[0];
|
||||||
if (user.phone) return user.phone;
|
if (typeof user.phone === 'string' && user.phone) return user.phone;
|
||||||
return '用户';
|
return '用户';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const PersonalCenterMenu = memo(({ user, handleLogout }) => {
|
|||||||
{/* 用户信息区 */}
|
{/* 用户信息区 */}
|
||||||
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
|
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
|
||||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||||
{user.phone && (
|
{typeof user.phone === 'string' && user.phone && (
|
||||||
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
||||||
)}
|
)}
|
||||||
{user.has_wechat && (
|
{user.has_wechat && (
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const TabletUserMenu = memo(({
|
|||||||
if (user.nickname) return user.nickname;
|
if (user.nickname) return user.nickname;
|
||||||
if (user.username) return user.username;
|
if (user.username) return user.username;
|
||||||
if (user.email) return user.email.split('@')[0];
|
if (user.email) return user.email.split('@')[0];
|
||||||
if (user.phone) return user.phone;
|
if (typeof user.phone === 'string' && user.phone) return user.phone;
|
||||||
return '用户';
|
return '用户';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ const TabletUserMenu = memo(({
|
|||||||
{/* 用户信息区 */}
|
{/* 用户信息区 */}
|
||||||
<Box px={3} py={2} borderBottom="1px" borderColor={borderColor}>
|
<Box px={3} py={2} borderBottom="1px" borderColor={borderColor}>
|
||||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||||
{user.phone && (
|
{typeof user.phone === 'string' && user.phone && (
|
||||||
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
<Text fontSize="xs" color="gray.500">{user.phone}</Text>
|
||||||
)}
|
)}
|
||||||
{user.has_wechat && (
|
{user.has_wechat && (
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const UserAvatar = forwardRef(({
|
|||||||
if (user.nickname) return user.nickname;
|
if (user.nickname) return user.nickname;
|
||||||
if (user.username) return user.username;
|
if (user.username) return user.username;
|
||||||
if (user.email) return user.email.split('@')[0];
|
if (user.email) return user.email.split('@')[0];
|
||||||
if (user.phone) return user.phone;
|
if (typeof user.phone === 'string' && user.phone) return user.phone;
|
||||||
return '用户';
|
return '用户';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -705,7 +705,7 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ position: 'relative', height: isMobile ? '450px' : '680px', width: '100%' }}>
|
<div style={{ position: 'relative', width: '100%' }}>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -736,7 +736,8 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
<span style={{ color: '#e0e0e0' }}>加载K线数据...</span>
|
<span style={{ color: '#e0e0e0' }}>加载K线数据...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={chartRef} style={{ width: '100%', height: '100%' }} />
|
{/* 使用 aspect-ratio 保持图表宽高比,K线图推荐 2.5:1 */}
|
||||||
|
<div ref={chartRef} style={{ width: '100%', aspectRatio: isMobile ? '1.8 / 1' : '2.5 / 1', minHeight: isMobile ? '280px' : '400px' }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -251,7 +251,8 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
|
|||||||
id={`kline-chart-${stock.stock_code}`}
|
id={`kline-chart-${stock.stock_code}`}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: `${CHART_HEIGHTS.main}px`,
|
minHeight: '300px',
|
||||||
|
height: 'min(400px, 60vh)',
|
||||||
opacity: showLoading ? 0.5 : 1,
|
opacity: showLoading ? 0.5 : 1,
|
||||||
transition: 'opacity 0.3s',
|
transition: 'opacity 0.3s',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -470,12 +470,13 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
<Modal isOpen={isOpen} onClose={onClose} size={size} isCentered>
|
<Modal isOpen={isOpen} onClose={onClose} size={size} isCentered>
|
||||||
<ModalOverlay bg="blackAlpha.700" />
|
<ModalOverlay bg="blackAlpha.700" />
|
||||||
<ModalContent
|
<ModalContent
|
||||||
maxW={isMobile ? '96vw' : '90vw'}
|
w={isMobile ? '96vw' : '90vw'}
|
||||||
maxH="85vh"
|
maxW={isMobile ? '96vw' : '1400px'}
|
||||||
borderRadius={isMobile ? '12px' : '8px'}
|
borderRadius={isMobile ? '12px' : '8px'}
|
||||||
bg="#1a1a1a"
|
bg="#1a1a1a"
|
||||||
border="2px solid #ffd700"
|
border="2px solid #ffd700"
|
||||||
boxShadow="0 0 30px rgba(255, 215, 0, 0.5)"
|
boxShadow="0 0 30px rgba(255, 215, 0, 0.5)"
|
||||||
|
overflow="visible"
|
||||||
>
|
>
|
||||||
<ModalHeader pb={isMobile ? 2 : 3} borderBottomWidth="1px" borderColor="#404040">
|
<ModalHeader pb={isMobile ? 2 : 3} borderBottomWidth="1px" borderColor="#404040">
|
||||||
<VStack align="flex-start" spacing={0}>
|
<VStack align="flex-start" spacing={0}>
|
||||||
@@ -498,7 +499,7 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box position="relative" h={isMobile ? '400px' : '600px'} w="100%">
|
<Box position="relative" w="100%">
|
||||||
{loading && (
|
{loading && (
|
||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
@@ -517,7 +518,15 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
</VStack>
|
</VStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
<div ref={chartRef} style={{ width: '100%', height: '100%' }} />
|
{/* 使用 aspect-ratio 保持图表宽高比,与日K线保持一致 */}
|
||||||
|
<Box
|
||||||
|
ref={chartRef}
|
||||||
|
w="100%"
|
||||||
|
sx={{
|
||||||
|
aspectRatio: isMobile ? '1.8 / 1' : '2.5 / 1',
|
||||||
|
}}
|
||||||
|
minH={isMobile ? '280px' : '400px'}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@@ -848,5 +848,18 @@ export const conceptHandlers = [
|
|||||||
lv1_id: lv1Id,
|
lv1_id: lv1Id,
|
||||||
lv2_id: lv2Id
|
lv2_id: lv2Id
|
||||||
});
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 热门概念静态数据文件(HeroPanel 使用)
|
||||||
|
http.get('/data/concept/latest.json', async () => {
|
||||||
|
await delay(200);
|
||||||
|
console.log('[Mock Concept] 获取热门概念静态数据');
|
||||||
|
|
||||||
|
const concepts = generatePopularConcepts(30);
|
||||||
|
|
||||||
|
return HttpResponse.json({
|
||||||
|
date: new Date().toISOString().split('T')[0],
|
||||||
|
results: concepts
|
||||||
|
});
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { http, HttpResponse } from 'msw';
|
|||||||
import { getEventRelatedStocks, generateMockEvents, generateHotEvents, generatePopularKeywords, generateDynamicNewsEvents } from '../data/events';
|
import { getEventRelatedStocks, generateMockEvents, generateHotEvents, generatePopularKeywords, generateDynamicNewsEvents } from '../data/events';
|
||||||
import { getMockFutureEvents, getMockEventCountsForMonth, toggleEventFollowStatus, isEventFollowed } from '../data/account';
|
import { getMockFutureEvents, getMockEventCountsForMonth, toggleEventFollowStatus, isEventFollowed } from '../data/account';
|
||||||
import { generatePopularConcepts } from './concept';
|
import { generatePopularConcepts } from './concept';
|
||||||
|
import { getCurrentUser } from '../data/users';
|
||||||
|
|
||||||
// 模拟网络延迟
|
// 模拟网络延迟
|
||||||
const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms));
|
const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
@@ -1498,15 +1499,19 @@ export const eventHandlers = [
|
|||||||
console.log('[Mock] 发表评论, eventId:', eventId, 'content:', body.content);
|
console.log('[Mock] 发表评论, eventId:', eventId, 'content:', body.content);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 获取当前登录用户信息
|
||||||
|
const currentUser = getCurrentUser();
|
||||||
|
|
||||||
// 模拟创建新评论
|
// 模拟创建新评论
|
||||||
const newComment = {
|
const newComment = {
|
||||||
id: `comment_${eventId}_${Date.now()}`,
|
id: `comment_${eventId}_${Date.now()}`,
|
||||||
content: body.content,
|
content: body.content,
|
||||||
content_type: body.content_type || 'text',
|
content_type: body.content_type || 'text',
|
||||||
author: {
|
author: {
|
||||||
id: 'current_user',
|
id: currentUser?.id || 'current_user',
|
||||||
username: '当前用户',
|
// 与导航区保持一致:优先显示昵称
|
||||||
avatar: null,
|
username: currentUser?.nickname || currentUser?.username || currentUser?.email || '当前用户',
|
||||||
|
avatar: currentUser?.avatar_url || null,
|
||||||
},
|
},
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
likes_count: 0,
|
likes_count: 0,
|
||||||
@@ -1536,4 +1541,45 @@ export const eventHandlers = [
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// 删除帖子/评论
|
||||||
|
http.delete('/api/posts/:postId', async ({ params }) => {
|
||||||
|
await delay(300);
|
||||||
|
const { postId } = params;
|
||||||
|
|
||||||
|
console.log('[Mock] 删除帖子, postId:', postId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 从内存存储中删除评论
|
||||||
|
let deleted = false;
|
||||||
|
for (const [eventId, comments] of commentsStore.entries()) {
|
||||||
|
const index = comments.findIndex(c => String(c.id) === String(postId));
|
||||||
|
if (index !== -1) {
|
||||||
|
comments.splice(index, 1);
|
||||||
|
deleted = true;
|
||||||
|
console.log('[Mock] 评论已从事件', eventId, '中删除');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
console.log('[Mock] 未找到评论,但仍返回成功(可能是乐观更新的评论)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '删除成功',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mock] 删除帖子失败:', error);
|
||||||
|
return HttpResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: '删除失败',
|
||||||
|
message: '系统错误,请稍后重试',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -210,7 +210,8 @@ export const eventService = {
|
|||||||
...post,
|
...post,
|
||||||
author: post.user ? {
|
author: post.user ? {
|
||||||
id: post.user.id,
|
id: post.user.id,
|
||||||
username: post.user.username,
|
// 与导航区保持一致:优先显示昵称
|
||||||
|
username: post.user.nickname || post.user.username,
|
||||||
avatar: post.user.avatar_url || post.user.avatar // 兼容 avatar_url 和 avatar
|
avatar: post.user.avatar_url || post.user.avatar // 兼容 avatar_url 和 avatar
|
||||||
} : {
|
} : {
|
||||||
id: 'anonymous',
|
id: 'anonymous',
|
||||||
|
|||||||
@@ -60,10 +60,12 @@ const PaginationControl = React.memo(({ currentPage, totalPages, onPageChange })
|
|||||||
pageNumbers.push(i);
|
pageNumbers.push(i);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 当前页在中间
|
// 当前页在中间:显示当前页前后各1个页码
|
||||||
pageNumbers.push(1);
|
pageNumbers.push(1);
|
||||||
pageNumbers.push('...');
|
pageNumbers.push('...');
|
||||||
pageNumbers.push(currentPage);
|
pageNumbers.push(currentPage - 1); // 前一页
|
||||||
|
pageNumbers.push(currentPage); // 当前页
|
||||||
|
pageNumbers.push(currentPage + 1); // 后一页
|
||||||
pageNumbers.push('...');
|
pageNumbers.push('...');
|
||||||
pageNumbers.push(totalPages);
|
pageNumbers.push(totalPages);
|
||||||
}
|
}
|
||||||
@@ -180,7 +182,7 @@ const PaginationControl = React.memo(({ currentPage, totalPages, onPageChange })
|
|||||||
{/* 输入框跳转 */}
|
{/* 输入框跳转 */}
|
||||||
<HStack spacing={1.5}>
|
<HStack spacing={1.5}>
|
||||||
<Text fontSize="xs" color="gray.600">
|
<Text fontSize="xs" color="gray.600">
|
||||||
跳转到
|
第
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -191,10 +193,13 @@ const PaginationControl = React.memo(({ currentPage, totalPages, onPageChange })
|
|||||||
value={jumpPage}
|
value={jumpPage}
|
||||||
onChange={(e) => setJumpPage(e.target.value)}
|
onChange={(e) => setJumpPage(e.target.value)}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
placeholder="页"
|
placeholder=""
|
||||||
bg={buttonBg}
|
bg={buttonBg}
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
/>
|
/>
|
||||||
|
<Text fontSize="xs" color="gray.600">
|
||||||
|
页
|
||||||
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
|
|||||||
@@ -569,8 +569,9 @@ const FlowingConcepts = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
animation={isPaused ? 'none' : `${animationName} ${duration}s linear infinite`}
|
animation={`${animationName} ${duration}s linear infinite`}
|
||||||
sx={{
|
sx={{
|
||||||
|
animationPlayState: isPaused ? 'paused' : 'running',
|
||||||
[`@keyframes ${animationName}`]: direction === 'left' ? {
|
[`@keyframes ${animationName}`]: direction === 'left' ? {
|
||||||
'0%': { transform: 'translateX(0)' },
|
'0%': { transform: 'translateX(0)' },
|
||||||
'100%': { transform: 'translateX(-50%)' },
|
'100%': { transform: 'translateX(-50%)' },
|
||||||
@@ -952,10 +953,6 @@ const HeroPanel = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack spacing={1} px={2} py={1} bg="rgba(255,255,255,0.05)" borderRadius="full">
|
|
||||||
<Box w="6px" h="6px" borderRadius="full" bg="gold" animation="pulse 2s infinite" />
|
|
||||||
<Text fontSize="10px" color="whiteAlpha.600">点击查看详情</Text>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* 流动式概念展示 */}
|
{/* 流动式概念展示 */}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;
|
||||||
transition: all 0.3s ease !important;
|
transition: all 0.3s ease !important;
|
||||||
z-index: 10 !important;
|
z-index: 10 !important;
|
||||||
|
user-select: none !important; /* 防止连续点击时选中文本 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-carousel-arrow:hover {
|
.custom-carousel-arrow:hover {
|
||||||
@@ -120,13 +121,38 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 重要度徽章 - 长方形圆角 */
|
||||||
.importance-badge {
|
.importance-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
left: 8px;
|
left: 8px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 2px 6px;
|
font-weight: 600;
|
||||||
border-radius: 4px;
|
color: #fff;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* S级 - 深红 */
|
||||||
|
.importance-s {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A级 - 浅红 */
|
||||||
|
.importance-a {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* B级 - 深橙 */
|
||||||
|
.importance-b {
|
||||||
|
background: #d35400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* C级 - 浅橙 */
|
||||||
|
.importance-c {
|
||||||
|
background: #f39c12;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card content */
|
/* Card content */
|
||||||
@@ -143,26 +169,18 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标题文字 - inline显示,可以换行 */
|
/* 标题文字 */
|
||||||
.event-title {
|
.event-title {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标签紧跟标题后面 */
|
/* 涨幅标签 - 底部显示 */
|
||||||
.event-tag {
|
|
||||||
display: inline;
|
|
||||||
margin-left: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-tag .ant-tag {
|
.event-tag .ant-tag {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
padding: 0 6px;
|
padding: 0 8px;
|
||||||
height: 18px;
|
height: 20px;
|
||||||
line-height: 18px;
|
line-height: 20px;
|
||||||
transform: scale(0.9);
|
margin: 0;
|
||||||
vertical-align: middle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 详情描述 - 三行省略 */
|
/* 详情描述 - 三行省略 */
|
||||||
@@ -183,18 +201,12 @@
|
|||||||
.event-footer {
|
.event-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #8c8c8c;
|
color: #8c8c8c;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.creator {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 时间样式 - 年月日高亮 */
|
/* 时间样式 - 年月日高亮 */
|
||||||
.time {
|
.time {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -90,6 +90,13 @@ const HotEvents = ({ events, onPageChange, onEventClick }) => {
|
|||||||
prevArrow: <CustomArrow direction="left" />,
|
prevArrow: <CustomArrow direction="left" />,
|
||||||
nextArrow: <CustomArrow direction="right" />,
|
nextArrow: <CustomArrow direction="right" />,
|
||||||
autoplay: false,
|
autoplay: false,
|
||||||
|
// 触控板/触摸优化
|
||||||
|
swipeToSlide: true, // 允许滑动到任意位置
|
||||||
|
touchThreshold: 10, // 滑动灵敏度
|
||||||
|
swipe: true, // 启用滑动
|
||||||
|
draggable: true, // PC 端拖拽支持
|
||||||
|
useCSS: true, // CSS 动画更流畅
|
||||||
|
cssEase: 'ease-out', // 滑动缓动效果
|
||||||
beforeChange: (_current, next) => {
|
beforeChange: (_current, next) => {
|
||||||
// 计算实际页码(考虑无限循环)
|
// 计算实际页码(考虑无限循环)
|
||||||
const actualPage = next % totalPages;
|
const actualPage = next % totalPages;
|
||||||
@@ -145,11 +152,9 @@ const HotEvents = ({ events, onPageChange, onEventClick }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{event.importance && (
|
{event.importance && (
|
||||||
<Badge
|
<span className={`importance-badge importance-${event.importance.toLowerCase()}`}>
|
||||||
className="importance-badge"
|
{event.importance}级
|
||||||
color={getImportanceColor(event.importance)}
|
</span>
|
||||||
text={`${event.importance}级`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -167,9 +172,6 @@ const HotEvents = ({ events, onPageChange, onEventClick }) => {
|
|||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<span className="event-tag">
|
|
||||||
{renderPriceChange(event.related_avg_chg)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
@@ -185,7 +187,9 @@ const HotEvents = ({ events, onPageChange, onEventClick }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="event-footer">
|
<div className="event-footer">
|
||||||
<span className="creator">{event.creator?.username || 'Anonymous'}</span>
|
<span className="event-tag">
|
||||||
|
{renderPriceChange(event.related_avg_chg)}
|
||||||
|
</span>
|
||||||
<span className="time">
|
<span className="time">
|
||||||
<span className="time-date">{dayjs(event.created_at).format('YYYY-MM-DD')}</span>
|
<span className="time-date">{dayjs(event.created_at).format('YYYY-MM-DD')}</span>
|
||||||
{' '}
|
{' '}
|
||||||
|
|||||||
@@ -400,13 +400,13 @@ export default function CenterDashboard() {
|
|||||||
{followingEvents.length}
|
{followingEvents.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Button
|
<IconButton
|
||||||
size="sm"
|
icon={<FiPlus />}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
onClick={() => navigate('/community')}
|
onClick={() => navigate('/community')}
|
||||||
>
|
aria-label="添加关注事件"
|
||||||
查看更多
|
/>
|
||||||
</Button>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody pt={0} flex="1" overflowY="auto">
|
<CardBody pt={0} flex="1" overflowY="auto">
|
||||||
|
|||||||
@@ -122,13 +122,20 @@ const HistoricalEvents = ({
|
|||||||
// navigate(`/event-detail/${event.id}`);
|
// navigate(`/event-detail/${event.id}`);
|
||||||
// };
|
// };
|
||||||
|
|
||||||
// 获取重要性颜色
|
// 获取重要性颜色(用于 Badge)
|
||||||
const getImportanceColor = (importance) => {
|
const getImportanceColor = (importance) => {
|
||||||
if (importance >= 4) return 'red';
|
if (importance >= 4) return 'red';
|
||||||
if (importance >= 2) return 'orange';
|
if (importance >= 2) return 'orange';
|
||||||
return 'green';
|
return 'green';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取重要性背景色(用于卡片背景)
|
||||||
|
const getImportanceBgColor = (importance) => {
|
||||||
|
if (importance >= 4) return 'rgba(239, 68, 68, 0.15)'; // 红色背景
|
||||||
|
if (importance >= 2) return 'rgba(245, 158, 11, 0.12)'; // 橙色背景
|
||||||
|
return 'rgba(34, 197, 94, 0.1)'; // 绿色背景
|
||||||
|
};
|
||||||
|
|
||||||
// 获取相关度颜色(1-10)
|
// 获取相关度颜色(1-10)
|
||||||
const getSimilarityColor = (similarity) => {
|
const getSimilarityColor = (similarity) => {
|
||||||
if (similarity >= 8) return 'green';
|
if (similarity >= 8) return 'green';
|
||||||
@@ -240,27 +247,15 @@ const HistoricalEvents = ({
|
|||||||
<VStack spacing={3} align="stretch">
|
<VStack spacing={3} align="stretch">
|
||||||
{events.map((event) => {
|
{events.map((event) => {
|
||||||
const importanceColor = getImportanceColor(event.importance);
|
const importanceColor = getImportanceColor(event.importance);
|
||||||
|
const importanceBgColor = getImportanceBgColor(event.importance);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
key={event.id}
|
key={event.id}
|
||||||
bg={cardBg}
|
bg={importanceBgColor}
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor="gray.500"
|
borderColor="gray.500"
|
||||||
borderRadius="lg"
|
borderRadius="lg"
|
||||||
position="relative"
|
|
||||||
overflow="visible"
|
|
||||||
_before={{
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: '3px',
|
|
||||||
bgGradient: 'linear(to-r, blue.400, purple.500, pink.500)',
|
|
||||||
borderTopLeftRadius: 'lg',
|
|
||||||
borderTopRightRadius: 'lg',
|
|
||||||
}}
|
|
||||||
transition="all 0.2s"
|
transition="all 0.2s"
|
||||||
>
|
>
|
||||||
<VStack align="stretch" spacing={3} p={4}>
|
<VStack align="stretch" spacing={3} p={4}>
|
||||||
|
|||||||
@@ -346,13 +346,32 @@ function getSankeyOption(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: { text: '事件影响力传导流向', left: 'center', top: 10 },
|
title: {
|
||||||
|
text: '事件影响力传导流向',
|
||||||
|
left: 'center',
|
||||||
|
top: 5,
|
||||||
|
textStyle: {
|
||||||
|
color: '#00d2d3',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}
|
||||||
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
triggerOn: 'mousemove',
|
triggerOn: 'mousemove',
|
||||||
|
backgroundColor: 'rgba(30, 30, 30, 0.95)',
|
||||||
|
borderColor: '#444',
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff'
|
||||||
|
},
|
||||||
formatter: (params) => {
|
formatter: (params) => {
|
||||||
if (params.dataType === 'node') {
|
if (params.dataType === 'node') {
|
||||||
return `<b>${params.name}</b><br/>类型: ${params.data.type || 'N/A'}<br/>层级: ${params.data.level || 'N/A'}<br/>点击查看详情`;
|
return `<div style="text-align: left;">` +
|
||||||
|
`<b style="font-size: 14px; color: #fff;">${params.name}</b><br/>` +
|
||||||
|
`<span style="color: #aaa;">类型:</span> <span style="color: #00d2d3;">${params.data.type || 'N/A'}</span><br/>` +
|
||||||
|
`<span style="color: #aaa;">层级:</span> <span style="color: #ffd700;">${params.data.level || 'N/A'}</span><br/>` +
|
||||||
|
`<span style="color: #4dabf7; text-decoration: underline; cursor: pointer;">🖱️ 点击查看详情</span>` +
|
||||||
|
`</div>`;
|
||||||
}
|
}
|
||||||
return params.name;
|
return params.name;
|
||||||
}
|
}
|
||||||
@@ -360,6 +379,10 @@ function getSankeyOption(data) {
|
|||||||
series: [{
|
series: [{
|
||||||
type: 'sankey',
|
type: 'sankey',
|
||||||
layout: 'none',
|
layout: 'none',
|
||||||
|
top: 50, // 给标题留出空间
|
||||||
|
bottom: 20,
|
||||||
|
left: 20,
|
||||||
|
right: 150, // 右侧留空间给标签
|
||||||
emphasis: { focus: 'adjacency' },
|
emphasis: { focus: 'adjacency' },
|
||||||
nodeAlign: 'justify',
|
nodeAlign: 'justify',
|
||||||
layoutIterations: 0,
|
layoutIterations: 0,
|
||||||
@@ -371,15 +394,67 @@ function getSankeyOption(data) {
|
|||||||
color: node.color,
|
color: node.color,
|
||||||
borderColor: node.level === 0 ? '#ffd700' : 'transparent',
|
borderColor: node.level === 0 ? '#ffd700' : 'transparent',
|
||||||
borderWidth: node.level === 0 ? 3 : 0
|
borderWidth: node.level === 0 ? 3 : 0
|
||||||
|
},
|
||||||
|
// 节点标签样式 - 突出显示,可点击感知
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
padding: [4, 8],
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
borderRadius: 4,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
borderWidth: 1,
|
||||||
|
// 添加下划线效果表示可点击
|
||||||
|
rich: {
|
||||||
|
clickable: {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
color: '#4dabf7'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
links: data.links.map(link => ({
|
links: data.links.map(link => ({
|
||||||
source: data.nodes[link.source]?.name,
|
source: data.nodes[link.source]?.name,
|
||||||
target: data.nodes[link.target]?.name,
|
target: data.nodes[link.target]?.name,
|
||||||
value: link.value,
|
value: link.value,
|
||||||
lineStyle: { color: 'source', opacity: 0.6, curveness: 0.5 }
|
// 降低链条透明度,让文字更突出
|
||||||
|
lineStyle: {
|
||||||
|
color: 'source',
|
||||||
|
opacity: 0.25, // 从0.6降低到0.25
|
||||||
|
curveness: 0.5
|
||||||
|
}
|
||||||
})),
|
})),
|
||||||
label: { color: 'rgba(0,0,0,0.7)', fontSize: 12 }
|
// 全局标签样式
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'right',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
padding: [4, 8],
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
borderRadius: 4,
|
||||||
|
borderColor: 'rgba(77, 171, 247, 0.5)',
|
||||||
|
borderWidth: 1,
|
||||||
|
formatter: '{b}'
|
||||||
|
},
|
||||||
|
// 高亮时的样式
|
||||||
|
emphasis: {
|
||||||
|
focus: 'adjacency',
|
||||||
|
label: {
|
||||||
|
color: '#4dabf7',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
borderColor: '#4dabf7',
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
opacity: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,6 +208,10 @@ export default function ProfilePage() {
|
|||||||
<Button
|
<Button
|
||||||
leftIcon={<CloseIcon />}
|
leftIcon={<CloseIcon />}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
colorScheme="gray"
|
||||||
|
color="gray.300"
|
||||||
|
borderColor="gray.500"
|
||||||
|
_hover={{ bg: 'gray.700', borderColor: 'gray.400' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|||||||
Reference in New Issue
Block a user