diff --git a/app.py b/app.py index 68e5ddf8..c6886497 100755 --- a/app.py +++ b/app.py @@ -4763,6 +4763,8 @@ def login_with_wechat(): 'username': user.username, 'nickname': user.nickname or user.username, 'email': user.email, + 'phone': user.phone, + 'phone_confirmed': bool(user.phone_confirmed), 'avatar_url': user.avatar_url, 'has_wechat': True, 'wechat_open_id': user.wechat_open_id, @@ -10958,6 +10960,7 @@ def create_event_post(event_id): 'created_at': post.created_at.isoformat(), 'user': { 'id': current_user.id, + 'nickname': current_user.nickname, # 添加昵称,与导航区保持一致 'username': current_user.username, 'avatar_url': current_user.avatar_url } diff --git a/src/components/EventCommentSection/CommentItem.js b/src/components/EventCommentSection/CommentItem.js index 6ebea5a8..9fe3ddf8 100644 --- a/src/components/EventCommentSection/CommentItem.js +++ b/src/components/EventCommentSection/CommentItem.js @@ -1,29 +1,63 @@ // src/components/EventCommentSection/CommentItem.js /** * 单条评论组件 - * 功能:显示用户头像、昵称、时间、评论内容 + * 功能:显示用户头像、昵称、时间、评论内容、删除按钮 */ -import React from 'react'; +import React, { useState } from 'react'; import { Box, HStack, VStack, Avatar, Text, + IconButton, useColorModeValue, + Tooltip, + AlertDialog, + AlertDialogBody, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogContent, + AlertDialogOverlay, + Button, + useDisclosure, } from '@chakra-ui/react'; +import { DeleteIcon } from '@chakra-ui/icons'; import dayjs from 'dayjs'; import '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 usernameColor = useColorModeValue('gray.800', 'gray.100'); const timeColor = useColorModeValue('gray.500', 'gray.400'); 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 now = dayjs(); @@ -46,43 +80,99 @@ const CommentItem = ({ comment }) => { }; return ( - - - {/* 用户头像 */} - + <> + + + {/* 用户头像 */} + - {/* 评论内容区 */} - - {/* 用户名和时间 */} - - - {comment.author?.username || 'Anonymous'} - - - {formatTime(comment.created_at)} - - + {/* 评论内容区 */} + + {/* 用户名和时间 */} + + + + {comment.author?.username || 'Anonymous'} + + + {formatTime(comment.created_at)} + + - {/* 评论内容 */} - - {comment.content} - - - - + {/* 删除按钮 - 只对自己的评论显示 */} + {canDelete && ( + + } + size="xs" + variant="ghost" + colorScheme="red" + aria-label="删除评论" + onClick={onOpen} + opacity={0.6} + _hover={{ opacity: 1 }} + /> + + )} + + + {/* 评论内容 */} + + {comment.content} + + + + + + {/* 删除确认对话框 */} + + + + + 删除评论 + + + + 确定要删除这条评论吗?此操作不可撤销。 + + + + + + + + + + ); }; diff --git a/src/components/EventCommentSection/CommentList.js b/src/components/EventCommentSection/CommentList.js index 0a08078f..0770e112 100644 --- a/src/components/EventCommentSection/CommentList.js +++ b/src/components/EventCommentSection/CommentList.js @@ -16,7 +16,7 @@ import { import { ChatIcon } from '@chakra-ui/icons'; import CommentItem from './CommentItem'; -const CommentList = ({ comments, loading }) => { +const CommentList = ({ comments, loading, currentUserId, onDelete }) => { const emptyTextColor = useColorModeValue('gray.500', 'gray.400'); const emptyBgColor = useColorModeValue('gray.50', 'gray.700'); @@ -58,7 +58,12 @@ const CommentList = ({ comments, loading }) => { return ( {comments.map((comment) => ( - + ))} ); diff --git a/src/components/EventCommentSection/EventCommentSection.tsx b/src/components/EventCommentSection/EventCommentSection.tsx index 47e4dad1..d69a56f6 100644 --- a/src/components/EventCommentSection/EventCommentSection.tsx +++ b/src/components/EventCommentSection/EventCommentSection.tsx @@ -144,8 +144,9 @@ const EventCommentSection: React.FC = ({ eventId }) => content_type: 'text', author: { 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(), likes_count: 0, @@ -187,6 +188,51 @@ const EventCommentSection: React.FC = ({ eventId }) => } }, [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 ( {/* 标题栏 */} @@ -203,7 +249,12 @@ const EventCommentSection: React.FC = ({ eventId }) => {/* 评论列表 */} - + {/* 加载更多按钮(仅当有更多评论时显示) */} diff --git a/src/components/Navbars/components/MobileDrawer/MobileDrawer.js b/src/components/Navbars/components/MobileDrawer/MobileDrawer.js index bc1218ed..d9f7a3e8 100644 --- a/src/components/Navbars/components/MobileDrawer/MobileDrawer.js +++ b/src/components/Navbars/components/MobileDrawer/MobileDrawer.js @@ -54,7 +54,7 @@ const MobileDrawer = memo(({ if (user.nickname) return user.nickname; if (user.username) return user.username; 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 '用户'; }; @@ -92,7 +92,7 @@ const MobileDrawer = memo(({ /> {getDisplayName()} - {user.phone && ( + {typeof user.phone === 'string' && user.phone && ( {user.phone} )} diff --git a/src/components/Navbars/components/Navigation/PersonalCenterMenu.js b/src/components/Navbars/components/Navigation/PersonalCenterMenu.js index 510f8281..167e4425 100644 --- a/src/components/Navbars/components/Navigation/PersonalCenterMenu.js +++ b/src/components/Navbars/components/Navigation/PersonalCenterMenu.js @@ -40,7 +40,7 @@ const PersonalCenterMenu = memo(({ user, handleLogout }) => { if (user.nickname) return user.nickname; if (user.username) return user.username; 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 '用户'; }; @@ -61,7 +61,7 @@ const PersonalCenterMenu = memo(({ user, handleLogout }) => { {/* 用户信息区 */} {getDisplayName()} - {user.phone && ( + {typeof user.phone === 'string' && user.phone && ( {user.phone} )} {user.has_wechat && ( diff --git a/src/components/Navbars/components/UserMenu/TabletUserMenu.js b/src/components/Navbars/components/UserMenu/TabletUserMenu.js index dc813313..ddb19987 100644 --- a/src/components/Navbars/components/UserMenu/TabletUserMenu.js +++ b/src/components/Navbars/components/UserMenu/TabletUserMenu.js @@ -46,7 +46,7 @@ const TabletUserMenu = memo(({ if (user.nickname) return user.nickname; if (user.username) return user.username; 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 '用户'; }; @@ -75,7 +75,7 @@ const TabletUserMenu = memo(({ {/* 用户信息区 */} {getDisplayName()} - {user.phone && ( + {typeof user.phone === 'string' && user.phone && ( {user.phone} )} {user.has_wechat && ( diff --git a/src/components/Navbars/components/UserMenu/UserAvatar.js b/src/components/Navbars/components/UserMenu/UserAvatar.js index 0ac08e3d..d1212203 100644 --- a/src/components/Navbars/components/UserMenu/UserAvatar.js +++ b/src/components/Navbars/components/UserMenu/UserAvatar.js @@ -29,7 +29,7 @@ const UserAvatar = forwardRef(({ if (user.nickname) return user.nickname; if (user.username) return user.username; 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 '用户'; }; diff --git a/src/components/StockChart/KLineChartModal.tsx b/src/components/StockChart/KLineChartModal.tsx index c303d517..07409313 100644 --- a/src/components/StockChart/KLineChartModal.tsx +++ b/src/components/StockChart/KLineChartModal.tsx @@ -705,7 +705,7 @@ const KLineChartModal: React.FC = ({ )} -
+
{loading && (
= ({ 加载K线数据...
)} -
+ {/* 使用 aspect-ratio 保持图表宽高比,K线图推荐 2.5:1 */} +
diff --git a/src/components/StockChart/StockChartKLineModal.tsx b/src/components/StockChart/StockChartKLineModal.tsx index de9f7d7f..745d0a54 100644 --- a/src/components/StockChart/StockChartKLineModal.tsx +++ b/src/components/StockChart/StockChartKLineModal.tsx @@ -251,7 +251,8 @@ const StockChartKLineModal: React.FC = ({ id={`kline-chart-${stock.stock_code}`} style={{ width: '100%', - height: `${CHART_HEIGHTS.main}px`, + minHeight: '300px', + height: 'min(400px, 60vh)', opacity: showLoading ? 0.5 : 1, transition: 'opacity 0.3s', }} diff --git a/src/components/StockChart/TimelineChartModal.tsx b/src/components/StockChart/TimelineChartModal.tsx index eb9eb809..66668e8c 100644 --- a/src/components/StockChart/TimelineChartModal.tsx +++ b/src/components/StockChart/TimelineChartModal.tsx @@ -470,12 +470,13 @@ const TimelineChartModal: React.FC = ({ @@ -498,7 +499,7 @@ const TimelineChartModal: React.FC = ({ )} - + {loading && ( = ({ )} -
+ {/* 使用 aspect-ratio 保持图表宽高比,与日K线保持一致 */} + diff --git a/src/mocks/handlers/concept.js b/src/mocks/handlers/concept.js index 1aa7e524..eb83ca6d 100644 --- a/src/mocks/handlers/concept.js +++ b/src/mocks/handlers/concept.js @@ -848,5 +848,18 @@ export const conceptHandlers = [ lv1_id: lv1Id, 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 + }); }) ]; diff --git a/src/mocks/handlers/event.js b/src/mocks/handlers/event.js index a8f488e5..1981def2 100644 --- a/src/mocks/handlers/event.js +++ b/src/mocks/handlers/event.js @@ -5,6 +5,7 @@ import { http, HttpResponse } from 'msw'; import { getEventRelatedStocks, generateMockEvents, generateHotEvents, generatePopularKeywords, generateDynamicNewsEvents } from '../data/events'; import { getMockFutureEvents, getMockEventCountsForMonth, toggleEventFollowStatus, isEventFollowed } from '../data/account'; import { generatePopularConcepts } from './concept'; +import { getCurrentUser } from '../data/users'; // 模拟网络延迟 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); try { + // 获取当前登录用户信息 + const currentUser = getCurrentUser(); + // 模拟创建新评论 const newComment = { id: `comment_${eventId}_${Date.now()}`, content: body.content, content_type: body.content_type || 'text', author: { - id: 'current_user', - username: '当前用户', - avatar: null, + id: currentUser?.id || 'current_user', + // 与导航区保持一致:优先显示昵称 + username: currentUser?.nickname || currentUser?.username || currentUser?.email || '当前用户', + avatar: currentUser?.avatar_url || null, }, created_at: new Date().toISOString(), 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 } + ); + } + }), ]; diff --git a/src/services/eventService.js b/src/services/eventService.js index 8c5ba657..f8e8fe77 100755 --- a/src/services/eventService.js +++ b/src/services/eventService.js @@ -210,7 +210,8 @@ export const eventService = { ...post, author: post.user ? { 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 } : { id: 'anonymous', diff --git a/src/views/Community/components/DynamicNews/PaginationControl.js b/src/views/Community/components/DynamicNews/PaginationControl.js index 9e171509..2f807f1b 100644 --- a/src/views/Community/components/DynamicNews/PaginationControl.js +++ b/src/views/Community/components/DynamicNews/PaginationControl.js @@ -60,10 +60,12 @@ const PaginationControl = React.memo(({ currentPage, totalPages, onPageChange }) pageNumbers.push(i); } } else { - // 当前页在中间 + // 当前页在中间:显示当前页前后各1个页码 pageNumbers.push(1); pageNumbers.push('...'); - pageNumbers.push(currentPage); + pageNumbers.push(currentPage - 1); // 前一页 + pageNumbers.push(currentPage); // 当前页 + pageNumbers.push(currentPage + 1); // 后一页 pageNumbers.push('...'); pageNumbers.push(totalPages); } @@ -180,7 +182,7 @@ const PaginationControl = React.memo(({ currentPage, totalPages, onPageChange }) {/* 输入框跳转 */} - 跳转到 + 第 setJumpPage(e.target.value)} onKeyPress={handleKeyPress} - placeholder="页" + placeholder="" bg={buttonBg} borderColor={borderColor} /> + + 页 +
} @@ -167,9 +172,6 @@ const HotEvents = ({ events, onPageChange, onEventClick }) => { )} - - {renderPriceChange(event.related_avg_chg)} -
{isMobile ? ( @@ -185,7 +187,9 @@ const HotEvents = ({ events, onPageChange, onEventClick }) => { )}
- {event.creator?.username || 'Anonymous'} + + {renderPriceChange(event.related_avg_chg)} + {dayjs(event.created_at).format('YYYY-MM-DD')} {' '} diff --git a/src/views/Dashboard/Center.js b/src/views/Dashboard/Center.js index 3e34c48e..4a71c0b0 100644 --- a/src/views/Dashboard/Center.js +++ b/src/views/Dashboard/Center.js @@ -400,13 +400,13 @@ export default function CenterDashboard() { {followingEvents.length} - + aria-label="添加关注事件" + /> diff --git a/src/views/EventDetail/components/HistoricalEvents.js b/src/views/EventDetail/components/HistoricalEvents.js index 366e163f..98745969 100644 --- a/src/views/EventDetail/components/HistoricalEvents.js +++ b/src/views/EventDetail/components/HistoricalEvents.js @@ -122,13 +122,20 @@ const HistoricalEvents = ({ // navigate(`/event-detail/${event.id}`); // }; - // 获取重要性颜色 + // 获取重要性颜色(用于 Badge) const getImportanceColor = (importance) => { if (importance >= 4) return 'red'; if (importance >= 2) return 'orange'; 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) const getSimilarityColor = (similarity) => { if (similarity >= 8) return 'green'; @@ -240,27 +247,15 @@ const HistoricalEvents = ({ {events.map((event) => { const importanceColor = getImportanceColor(event.importance); + const importanceBgColor = getImportanceBgColor(event.importance); return ( diff --git a/src/views/EventDetail/components/TransmissionChainAnalysis.js b/src/views/EventDetail/components/TransmissionChainAnalysis.js index f87efc8c..412fdf43 100644 --- a/src/views/EventDetail/components/TransmissionChainAnalysis.js +++ b/src/views/EventDetail/components/TransmissionChainAnalysis.js @@ -339,20 +339,39 @@ function getGraphOption(data) { }; } -// 桑基图配置 +// 桑基图配置 function getSankeyOption(data) { if (!data || !data.nodes || !data.links) { return { title: { text: '暂无桑基图数据', left: 'center', top: 'center' } }; } - return { - title: { text: '事件影响力传导流向', left: 'center', top: 10 }, - tooltip: { - trigger: 'item', + return { + title: { + text: '事件影响力传导流向', + left: 'center', + top: 5, + textStyle: { + color: '#00d2d3', + fontSize: 16, + fontWeight: 'bold' + } + }, + tooltip: { + trigger: 'item', triggerOn: 'mousemove', + backgroundColor: 'rgba(30, 30, 30, 0.95)', + borderColor: '#444', + textStyle: { + color: '#fff' + }, formatter: (params) => { if (params.dataType === 'node') { - return `${params.name}
类型: ${params.data.type || 'N/A'}
层级: ${params.data.level || 'N/A'}
点击查看详情`; + return `
` + + `${params.name}
` + + `类型: ${params.data.type || 'N/A'}
` + + `层级: ${params.data.level || 'N/A'}
` + + `🖱️ 点击查看详情` + + `
`; } return params.name; } @@ -360,6 +379,10 @@ function getSankeyOption(data) { series: [{ type: 'sankey', layout: 'none', + top: 50, // 给标题留出空间 + bottom: 20, + left: 20, + right: 150, // 右侧留空间给标签 emphasis: { focus: 'adjacency' }, nodeAlign: 'justify', layoutIterations: 0, @@ -371,15 +394,67 @@ function getSankeyOption(data) { color: node.color, borderColor: node.level === 0 ? '#ffd700' : 'transparent', 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 => ({ source: data.nodes[link.source]?.name, target: data.nodes[link.target]?.name, 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 + } + } }] }; } diff --git a/src/views/Profile/ProfilePage.js b/src/views/Profile/ProfilePage.js index 8751fa0d..4df65e45 100644 --- a/src/views/Profile/ProfilePage.js +++ b/src/views/Profile/ProfilePage.js @@ -208,6 +208,10 @@ export default function ProfilePage() {